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 分析系统通知管理服务(NMS) -> 正文阅读

[移动开发]基于Android 10 分析系统通知管理服务(NMS)

基于Android 10 分析系统通知管理服务(NMS)

相关源码文件如下:

frameworks/base/core/java/android/app/
	- NotificationManager.java
	- Notification.java
	- NotificationChannel.java
	
frameworks/base/core/java/android/service/notification/
	- NotificationListenerService.java

frameworks/base/services/core/java/com/android/server/notification/
	- NotificationManagerService.java
	
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/
	- NotificationListener.java
	
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/
	- NotificationEntryManager.java

一. 概述

Android应用除了WMS和AMS的管理服务之外,还有通知管理服务(NMS)也是非常重要的,通知是应用界面之外向用户显示的界面。

1.1 通知使用示例

1.1.1 创建Channel

val nm: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    val channel =
        NotificationChannel("123", "TestChannel", NotificationManager.IMPORTANCE_HIGH)
    nm.createNotificationChannel(channel)
}

1.1.2 创建通知

val notification: Notification = NotificationCompat.Builder(this, "123")
    .setContentText("ContentText")
    .setTicker("Ticker")
    .setGroupSummary(true)
    .setContentTitle("Hello")
    .setContentInfo("ContentInfo")
    .setSmallIcon(R.mipmap.ic_launcher)
    .build()

1.1.3 发送通知

val nm: NotificationManager =
            getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.notify(notifyId, notification)

1.1.4 取消通知

val nm: NotificationManager =
            getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.cancel(notifyId) // 取消指定通知
nm.cancelAll(); // 取消所有通知

1.2 架构图

1.2.1 核心类图

1.2.2 通知处理流程图

alt 通知处理流程图

二. 通知发送原理分析

由于分析是基于调用栈分析,所以建议读者按章节顺序阅读。

2.1 NM.notify

源码文件: /frameworks/base/core/java/android/app/NotificationManager.java

运行在三方App进程中

public void notify(int id, Notification notification)
{
    notify(null, id, notification);
}
public void notify(String tag, int id, Notification notification)
{
    notifyAsUser(tag, id, notification, mContext.getUser());
}
public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
{
    // 获取通知的代理对象
    INotificationManager service = getService();
    String pkg = mContext.getPackageName();
    try {
        if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
        // 通过Binder调用NMS的enqueueNotificationWithTag方法 [2.2]
        service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                fixNotification(notification), user.getIdentifier());
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}
private Notification fixNotification(Notification notification) {
    String pkg = mContext.getPackageName();
    // 将ApplicationInfo信息添加到Notification中的extras字段中
    Notification.addFieldsFromContext(mContext, notification);
    ...
    fixLegacySmallIcon(notification, pkg);
    // 对于Android API 22以上版本需要添加SmallIcon,否则将抛出异常
    if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
        if (notification.getSmallIcon() == null) {
            throw new IllegalArgumentException("Invalid notification (no valid small icon): "
                    + notification);
        }
    }
    notification.reduceImageSizes(mContext);
    ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
    boolean isLowRam = am.isLowRamDevice();
    return Builder.maybeCloneStrippedForDelivery(notification, isLowRam, mContext);
}

在App进程中调用NotificationManager类的notify()方法,最终会通过Binder调用system_server进程中的NotifcationManagerService(简称NMS),执行enqueueNotificationWithTag方法。

2.2 NMS.enqueueNotificationWithTag

源码文件:/frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

运行在system_server进程中

final IBinder mService = new INotificationManager.Stub() {
    @Override
    public void enqueueNotificationWithTag(String pkg, String opPkg, String tag, int id, Notification notification, int userId) throws RemoteException {
        enqueueNotificationInternal(pkg, opPkg, Binder.getCallingUid(),
                Binder.getCallingPid(), tag, id, notification, userId);
    }
}

void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid,
            final int callingPid, final String tag, final int id, final Notification notification,
            int incomingUserId) {
    ...

    final int userId = ActivityManager.handleIncomingUser(callingPid,
            callingUid, incomingUserId, true, false, "enqueueNotification", pkg);
    final UserHandle user = UserHandle.of(userId);

    // 如果调用 uid 没有发布权限,则可以抛出 SecurityException
    final int notificationUid = resolveNotificationUid(opPkg, pkg, callingUid, userId);
    ...
    try {
        // 修复通知信息
        fixNotification(notification, pkg, userId);
    } catch (NameNotFoundException e) {
        Slog.e(TAG, "Cannot create a context for sending app", e);
        return;
    }
    ...
    String channelId = notification.getChannelId();
    final NotificationChannel channel = mPreferencesHelper.getNotificationChannel(pkg,
            notificationUid, channelId, false /* includeDeleted */);
    // 如果Channel不存在,则提示错误。在发送通知之前需要创建Channel
    if (channel == null) {
        ...
        boolean appNotificationsOff = mPreferencesHelper.getImportance(pkg, notificationUid)
                == NotificationManager.IMPORTANCE_NONE;
        if (!appNotificationsOff) {
            doChannelWarningToast("Developer warning for package \"" + pkg + "\"\n" +
                    "Failed to post notification on channel \"" + channelId + "\"\n" +
                    "See log for more details");
        }
        return;
    }
	// 将通知信息和package信息封装到StatusBarNotification对象中
    // StatusBarNotification构造函数中会初始化key和groupKey属性
    final StatusBarNotification n = new StatusBarNotification(
            pkg, opPkg, id, tag, notificationUid, callingPid, notification,
            user, null, System.currentTimeMillis());
    // 将Channel和StatusBarNotification信息封装到NotificationRecord对象中
    final NotificationRecord r = new NotificationRecord(getContext(), n, channel);
    r.setIsAppImportanceLocked(mPreferencesHelper.getIsAppImportanceLocked(pkg, callingUid));

    if ((notification.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
       ... // 省略处理前台服务逻辑
    }
	// 检查是否可以发布通知。 检查速率限制器、贪睡助手和阻塞。
    if (!checkDisqualifyingFeatures(userId, notificationUid, id, tag, r,
            r.sbn.getOverrideGroupKey() != null)) {
        return;
    }
   	...
    // 通过WorkHandler提交给Runnable处理。 [见2.3]
    mHandler.post(new EnqueueNotificationRunnable(userId, r));
}

这个过程主要是创建NotificationRecord对象保存Notification信息,最后通过Handler提交给EnqueueNotificationRunnable处理。

  • key规则是“UserHandle#getIdentifier()|pkg|notifyId|tag|uid”
  • mHandler是WorkerHandler的实例对象,在NMS启动后初始化。

2.3 EnqueueNotificationRunnable

源码文件:/frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

运行在system_server进程中

protected class EnqueueNotificationRunnable implements Runnable {
    private final NotificationRecord r;
    private final int userId;
    EnqueueNotificationRunnable(int userId, NotificationRecord r) {
        this.userId = userId;
        this.r = r;
    };
    @Override
    public void run() {
        synchronized (mNotificationLock) {
            // 放入列表中
            mEnqueuedNotifications.add(r);
            scheduleTimeoutLocked(r);
            final StatusBarNotification n = r.sbn;
            ...
            // 处理气泡
            flagNotificationForBubbles(r, pkg, callingUid, old);
            // 处理分组通知
            handleGroupedNotificationLocked(r, old, callingUid, callingPid);
            ...
            // 将通知告诉助理服务
            if (mAssistants.isEnabled()) {
                mAssistants.onNotificationEnqueuedLocked(r);
                // [2.4]
                mHandler.postDelayed(new PostNotificationRunnable(r.getKey()),
                        DELAY_FOR_ASSISTANT_TIME);
            } else {
                // [2.4]
                mHandler.post(new PostNotificationRunnable(r.getKey()));
            }
        }
        ...
    }
}
  • 首先将NotificationRecord放入队列中待处理。
  • 处理气泡通知和分组通知
  • 最后通过Handler提交给PostNotificationRunnable处理,并且传递了key值,而非NotificationRecord对象。

2.4 PostNotificationRunnable

源码文件:/frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

运行在system_server进程中

protected class PostNotificationRunnable implements Runnable {
    private final String key;
    PostNotificationRunnable(String key) {
        this.key = key;
    }
    @Override
    public void run() {
        synchronized (mNotificationLock) {
            try {
            	// 通过Key值找出NotificationRecord对象
                NotificationRecord r = null;
                int N = mEnqueuedNotifications.size();
                for (int i = 0; i < N; i++) {
                    final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
                    if (Objects.equals(key, enqueued.getKey())) {
                        r = enqueued;
                        break;
                    }
                }
                // 未找到情况下则提示异常,且丢弃这次任务
                if (r == null) {
                    Slog.i(TAG, "Cannot find enqueued record for key: " + key);
                    return;
                }
				// 判断是否被阻止通知,主要通过优先级和频道分组通知判断
                if (isBlocked(r)) {
                    Slog.i(TAG, "notification blocked by assistant request");
                    return;
                }
				...
                // 相同Key的上一次的通知信息
                NotificationRecord old = mNotificationsByKey.get(key);
                final StatusBarNotification n = r.sbn;
                final Notification notification = n.getNotification();
                // 从通知列表查询是否存在该通知
                int index = indexOfNotificationLocked(n.getKey());
                if (index < 0) {
                    mNotificationList.add(r);
                    mUsageStats.registerPostedByApp(r);
                    r.setInterruptive(isVisuallyInterruptive(null, r));
                } else {
                    old = mNotificationList.get(index);
                    mNotificationList.set(index, r);
                    mUsageStats.registerUpdatedByApp(r, old);
                    // 确保不会丢失前台服务状态。
                    notification.flags |=
                            old.getNotification().flags & FLAG_FOREGROUND_SERVICE;
                    r.isUpdate = true;
                    r.setTextChanged(isVisuallyInterruptive(old, r));
                }
				// 将NotificationRecord存入Map中
                mNotificationsByKey.put(n.getKey(), r);
                // 如果是前台服务的通知,则添加不允许被清除和正在运行的标签
                if ((notification.flags & FLAG_FOREGROUND_SERVICE) != 0) {
                    notification.flags |= FLAG_ONGOING_EVENT
                            | FLAG_NO_CLEAR;
                }
                mRankingHelper.extractSignals(r);
                mRankingHelper.sort(mNotificationList);
                if (!r.isHidden()) {
                    buzzBeepBlinkLocked(r);
                }
                if (notification.getSmallIcon() != null) {
                    // 通知观察者
                    StatusBarNotification oldSbn = (old != null) ? old.sbn : null;
                    mListeners.notifyPostedLocked(r, old); // 见[2.5]
                    if ((oldSbn == null || !Objects.equals(oldSbn.getGroup(), n.getGroup()))
                            && !isCritical(r)) {
                        mHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                mGroupHelper.onNotificationPosted(
                                        n, hasAutoGroupSummaryLocked(n));
                            }
                        });
                    }
                } else {
                    // 不发布没有小图标的通知
                    if (old != null && !old.isCanceled) {
                        mListeners.notifyRemovedLocked(r,
                                NotificationListenerService.REASON_ERROR, r.getStats());
                        mHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                mGroupHelper.onNotificationRemoved(n);
                            }
                        });
                    }
                }

                maybeRecordInterruptionLocked(r);
            } finally {
                // 最后在列表中移除
                int N = mEnqueuedNotifications.size();
                for (int i = 0; i < N; i++) {
                    final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
                    if (Objects.equals(key, enqueued.getKey())) {
                        mEnqueuedNotifications.remove(i);
                        break;
                    }
                }
            }
        }
    }
}
  • 通过mEnqueuedNotifications 列表找出本次通知信息NotificationRecord。
  • 通过mNotificationsByKey列表找出上一个通知信息。
  • 通过mNotificationList列表找出当前key的通知信息,且确保前台服务状态不丢失。
  • 前台服务单独添加两个Flag (FLAG_ONGOING_EVENT | FLAG_NO_CLEAR) 。
  • 没有小图标的通知不显示
  • 最后通知观察者
  • 此mListeners为NotificationListeners对象,是NMS内部类并且在NMS初始化创建。

2.5 NotificationListeners.notifyPostedLocked

源码文件:/frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

运行在system_server进程中

public class NotificationListeners extends ManagedServices {
 	// ...
    @GuardedBy("mNotificationLock")
    public void notifyPostedLocked(NotificationRecord r, NotificationRecord old) {
        notifyPostedLocked(r, old, true);
    }
    @GuardedBy("mNotificationLock")
    private void notifyPostedLocked(NotificationRecord r, NotificationRecord old,
            boolean notifyAllListeners) {
        // Lazily initialized snapshots of the notification.
        StatusBarNotification sbn = r.sbn;
        StatusBarNotification oldSbn = (old != null) ? old.sbn : null;
        TrimCache trimCache = new TrimCache(sbn);
		// 遍历整个ManagedServices中的所有ManagedServiceInfo,发送给所有观察者,主要观察者是SystemUI
        for (final ManagedServiceInfo info : getServices()) {
            boolean sbnVisible = isVisibleToListener(sbn, info);
            boolean oldSbnVisible = oldSbn != null ? isVisibleToListener(oldSbn, info) : false;
            // 通过监听者判断是否需要显示该通知
            if (!oldSbnVisible && !sbnVisible) {
                continue;
            }
            // 通知被隐藏,则忽略
            if (r.isHidden() && info.targetSdkVersion < Build.VERSION_CODES.P) {
                continue;
            }
            if (!notifyAllListeners && info.targetSdkVersion >= Build.VERSION_CODES.P) {
                continue;
            }
            final NotificationRankingUpdate update = makeRankingUpdateLocked(info);
            // 通知变得不可见之后移除老的通知
            if (oldSbnVisible && !sbnVisible) {
                final StatusBarNotification oldSbnLightClone = oldSbn.cloneLight();
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        notifyRemoved(
                                info, oldSbnLightClone, update, null, REASON_USER_STOPPED);
                    }
                });
                continue;
            }
            // 在通知侦听器之前授予访问权限
            final int targetUserId = (info.userid == UserHandle.USER_ALL)
                    ? UserHandle.USER_SYSTEM : info.userid;
            updateUriPermissions(r, old, info.component.getPackageName(), targetUserId);

            final StatusBarNotification sbnToPost = trimCache.ForListener(info);
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    notifyPosted(info, sbnToPost, update);
                }
            });
        }
    }
    // 发出通知
    private void notifyPosted(final ManagedServiceInfo info,
                              final StatusBarNotification sbn, NotificationRankingUpdate rankingUpdate) {
        final INotificationListener listener = (INotificationListener) info.service;
        // 包装StatusBarNotification
        StatusBarNotificationHolder sbnHolder = new StatusBarNotificationHolder(sbn);
        try {
            listener.onNotificationPosted(sbnHolder, rankingUpdate); // [3.1]
        } catch (RemoteException ex) {
            Slog.e(TAG, "unable to notify listener (posted): " + listener, ex);
        }
    }
	// ...
}
  • 此处的listener来自与ManagedServiceInfo的service成员变量。
  • listener数据类型是NotificationListenerWrapper的代理对象
  • sbnHolder的数据类型是StatusBarNotificationHolder,继承于IStatusBarNotificationHolder.Stub对象。

三. SysetmUI

3.1 NotificationListenerWrapper.onNotificationPosted

源码文件:/frameworks/base/core/java/android/service/notification/NotificationListenerService.java

运行在com.android.systemui进程中

protected class NotificationListenerWrapper extends INotificationListener.Stub {
    @Override
    public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder,
            NotificationRankingUpdate update) {
        StatusBarNotification sbn;
        try {
            sbn = sbnHolder.get(); // 通过Binder向 system_server 读取sbn对象。
        } catch (RemoteException e) {
            Log.w(TAG, "onNotificationPosted: Error receiving StatusBarNotification", e);
            return;
        }
        // ...
        synchronized (mLock) {
            applyUpdateLocked(update);
            if (sbn != null) {
                SomeArgs args = SomeArgs.obtain();
                args.arg1 = sbn;
                args.arg2 = mRankingMap;
                mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_POSTED,
                        args).sendToTarget(); // [2.7]
            } else {
                // still pass along the ranking map, it may contain other information
                mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_RANKING_UPDATE,
                        mRankingMap).sendToTarget();
            }
        }
    }
}
  • 通过Binder向 system_server 读取sbn对象。
  • 提交给Handler处理,此Handler运行在systemui进程的主线程中。

3.2 MyHandler

源码路径: /frameworks/base/core/java/android/service/notification/NotificationListenerService.java

运行在com.android.systemui进程中

private final class MyHandler extends Handler {
    public static final int MSG_ON_NOTIFICATION_POSTED = 1;
    public MyHandler(Looper looper) {
        super(looper, null, false);
    }
    @Override
    public void handleMessage(Message msg) {
        if (!isConnected) {
            return;
        }
        switch (msg.what) {
            case MSG_ON_NOTIFICATION_POSTED: {
                SomeArgs args = (SomeArgs) msg.obj;
                StatusBarNotification sbn = (StatusBarNotification) args.arg1;
                RankingMap rankingMap = (RankingMap) args.arg2;
                args.recycle();
                onNotificationPosted(sbn, rankingMap); // [3.3]
            } break;
            // ...
        }
    }
}
  • 最后调用NotificationListenerService实例对象的onNotificationPosted()
  • MyHandler为NotificationListenerService内部类

3.3 NotificationListener.onNotificationPosted

源码文件:/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java

运行在com.android.systemui进程中

public class NotificationListener extends NotificationListenerWithPlugins {
    private static final String TAG = "NotificationListener";

    // Dependencies:
    private final NotificationRemoteInputManager mRemoteInputManager =
            Dependency.get(NotificationRemoteInputManager.class);
    private final NotificationEntryManager mEntryManager =
            Dependency.get(NotificationEntryManager.class);
    private final NotificationGroupManager mGroupManager =
            Dependency.get(NotificationGroupManager.class);

    private final ArrayList<NotificationSettingsListener> mSettingsListeners = new ArrayList<>();
    private final Context mContext;

	// ...

    @Override
    public void onNotificationPosted(final StatusBarNotification sbn,
            final RankingMap rankingMap) {
        // ...
        if (sbn != null && !onPluginNotificationPosted(sbn, rankingMap)) {
            Dependency.get(Dependency.MAIN_HANDLER).post(() -> {
                // 处理远端输入(手表)
                processForRemoteInput(sbn.getNotification(), mContext);
                String key = sbn.getKey();
                // 判断是否已经存在当前key,存在则更新,否则新增
                boolean isUpdate =
                        mEntryManager.getNotificationData().get(key) != null;
                if (!ENABLE_CHILD_NOTIFICATIONS
                        && mGroupManager.isChildInGroupWithSummary(sbn)) {
                    // 删除现有通知,以避免陈旧数据
                    if (isUpdate) {
                        mEntryManager.removeNotification(key, rankingMap, UNDEFINED_DISMISS_REASON);
                    } else {
                        mEntryManager.getNotificationData()
                                .updateRanking(rankingMap);
                    }
                    return;
                }
                if (isUpdate) {
                    // 更新通知
                    mEntryManager.updateNotification(sbn, rankingMap);
                } else {
                    // 新增通知
                    mEntryManager.addNotification(sbn, rankingMap); // [3.4]
                }
            });
        }
    }
}
  • 最终通过 isUpdate 来区分是更新还是新增通知。

3.4 NotificationEntryManager.addNotification

源码文件:/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java

运行在com.android.systemui进程中

public class NotificationEntryManager implements
        Dumpable,
        NotificationContentInflater.InflationCallback,
        NotificationUpdateHandler,
        VisualStabilityManager.Callback {
    // ...

    private void addNotificationInternal(StatusBarNotification notification,
            NotificationListenerService.RankingMap rankingMap) throws InflationException {
        String key = notification.getKey();
        if (DEBUG) {
            Log.d(TAG, "addNotification key=" + key);
        }
        mNotificationData.updateRanking(rankingMap);
        NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking();
        rankingMap.getRanking(key, ranking);

        NotificationEntry entry = new NotificationEntry(notification, ranking);
        Dependency.get(LeakDetector.class).trackInstance(entry);
        // 构建布局
        requireBinder().inflateViews(entry, () -> performRemoveNotification(notification,
                REASON_CANCEL));

        abortExistingInflation(key);

        mPendingNotifications.put(key, entry);
        for (NotificationEntryListener listener : mNotificationEntryListeners) {
            listener.onPendingEntryAdded(entry);
        }
    }

    @Override
    public void addNotification(StatusBarNotification notification,
            NotificationListenerService.RankingMap ranking) {
        try {
            addNotificationInternal(notification, ranking);
        } catch (InflationException e) {
            handleInflationException(notification, e);
        }
    }
}

四. 小结

源码分析中共涉及三个进程,分别是三方app进程、system_server进程、SystermUI进程,进程之间通过Binder通信。

持续更新…

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-29 09:28:21  更:2021-08-29 09:29:20 
 
开发: 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/23 13:46:29-

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