【问题标题】:Multiple Bitmap update on same android SurfaceView同一android SurfaceView上的多个位图更新
【发布时间】:2015-06-29 16:40:32
【问题描述】:

最近我尝试编写一个应用程序,该应用程序从 USB 相机(需要显示为预览)捕获帧缓冲区,以及需要在预览上重叠的图像处理输出。谁能给我一些指示如何开始?此外,我需要在预览中绘制一些矩形。

我尝试使用多个SurfaceView 绘制但未成功。谁能帮帮我?

【问题讨论】:

标签: android bitmap surfaceview


【解决方案1】:

您需要位图和图像视图引用计数。 Android 将图像数据保存在原生数组中,vm GC 运行时不会自动回收。 Vm 部分是垃圾收集的一种方式,而本机部分是另一种方式,而且要晚得多。您的应用可能很快就会耗尽内存。

这里有一组可以提供帮助的类。我想我是从 android image tutorial 得到的,为了自己的方便做了一些修改。

package com.example.android.streaming.ui.cache;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.util.AttributeSet;
import android.widget.ImageView;

/**
 * Sub-class of ImageView which automatically notifies the drawable when it is
 * being displayed.
 */
public class RecyclingImageView extends ImageView {
    public RecyclingImageView(Context context) {
        super(context);
    }

    public RecyclingImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * @see android.widget.ImageView#onDetachedFromWindow()
     */
    @Override
    protected void onDetachedFromWindow() {
        // This has been detached from Window, so clear the drawable
        setImageDrawable(null);

        super.onDetachedFromWindow();
    }

    /**
     * @see android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)
     */
    @Override
    public void setImageDrawable(Drawable drawable) {
        // Keep hold of previous Drawable
        final Drawable previousDrawable = getDrawable();

        // Call super to set new Drawable
        super.setImageDrawable(drawable);

        // Notify new Drawable that it is being displayed
        notifyDrawable(drawable, true);

        // Notify old Drawable so it is no longer being displayed
        notifyDrawable(previousDrawable, false);
    }

    @Override
    public void setImageResource(int resId) {
        // Keep hold of previous Drawable
        final Drawable previousDrawable = getDrawable();
        super.setImageResource(resId);

        // Notify new Drawable that it is being displayed
        final Drawable newDrawable = getDrawable();
        notifyDrawable(newDrawable, true);

        // Notify old Drawable so it is no longer being displayed
        notifyDrawable(previousDrawable, false);
    }

    /**
     * Notifies the drawable that it's displayed state has changed.
     * 
     * @param drawable
     * @param isDisplayed
     */
    private static void notifyDrawable(Drawable drawable, final boolean isDisplayed) {
        if (drawable != null) {
            if (drawable instanceof RecyclingBitmapDrawable) {
                // The drawable is a CountingBitmapDrawable, so notify it
                ((RecyclingBitmapDrawable) drawable).setIsDisplayed(isDisplayed);
            } else if (drawable instanceof LayerDrawable) {
                // The drawable is a LayerDrawable, so recurse on each layer
                LayerDrawable layerDrawable = (LayerDrawable) drawable;
                for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) {
                    notifyDrawable(layerDrawable.getDrawable(i), isDisplayed);
                }
            }
        }
    }

}

这是另一个,位图本身。

package com.example.android.streaming.ui.cache;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.util.Log;

import com.example.android.streaming.StreamingApp;
import com.vg.hangwith.BuildConfig;

/**
 * A BitmapDrawable that keeps track of whether it is being displayed or cached.
 * When the drawable is no longer being displayed or cached,
 * {@link Bitmap#recycle() recycle()} will be called on this drawable's bitmap.
 */
public class RecyclingBitmapDrawable extends BitmapDrawable {
    private int cacheRefCount = 0;
    private int displayRefCount = 0;

    private boolean hasBeenDisplayed;

    public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) {
        super(res, bitmap);
    }

    /**
     * Notify the drawable that the displayed state has changed. Internally a
     * count is kept so that the drawable knows when it is no longer being
     * displayed.
     * 
     * @param isDisplayed
     *            - Whether the drawable is being displayed or not
     */
    public void setIsDisplayed(boolean isDisplayed) {
        synchronized (this) {
            if (isDisplayed) {
                displayRefCount++;
                hasBeenDisplayed = true;
            } else {
                displayRefCount--;
            }
        }

        // Check to see if recycle() can be called
        checkState();
    }

    /**
     * Notify the drawable that the cache state has changed. Internally a count
     * is kept so that the drawable knows when it is no longer being cached.
     * 
     * @param isCached
     *            - Whether the drawable is being cached or not
     */
    public void setIsCached(boolean isCached) {
        synchronized (this) {
            if (isCached) {
                cacheRefCount++;
            } else {
                cacheRefCount--;
            }
        }

        // Check to see if recycle() can be called
        checkState();
    }

    private synchronized void checkState() {
        // If the drawable cache and display ref counts = 0, and this drawable
        // has been displayed, then recycle
        if (cacheRefCount <= 0 && displayRefCount <= 0 && hasBeenDisplayed && hasValidBitmap()) {
            if (BuildConfig.DEBUG)
                Log.d(StreamingApp.TAG, "No longer being used or cached so recycling. " + toString());
            getBitmap().recycle();
        }
    }

    private synchronized boolean hasValidBitmap() {
        Bitmap bitmap = getBitmap();
        return bitmap != null && !bitmap.isRecycled();
    }

}

现在,你的活动,无论它做什么,如果它需要呈现可回收的图像,你在 xml res 中添加这个:

<com.example.android.streaming.ui.cache.RecyclingImageView
        android:id="@+id/ad_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        android:background="@drawable/bkgd_whitegradient"
        android:contentDescription="@string/dummy_desc"
        android:padding="20dip"/>

这只是一个例子,id,背景,你需要什么都可以。

    final RecyclingImageView adImage = (RecyclingImageView) findViewById(R.id.ad_image);
    adImage.setImageDrawable(new RecyclingBitmapDrawable(getResources(), getBitmap(this)));
    adImage.setVisibility(View.VISIBLE);

注意getBitmap(),这是一个例子。您应该以您需要的方式实施它。它返回位图实例。在您的情况下,此位图将由您从相机收到的字节数组创建。让我们在这里也尝试一下。

接下来,我有一个用于在我的应用程序中管理头像的类(一长串用户就是一个很好的例子)。它有许多有用的静态方法,所以你可能不需要创建它。

package com.example.android.streaming.ui.cache;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.json.JSONObject;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.util.Log;
import android.util.LruCache;

import com.example.android.streaming.StreamingApp;
import com.example.android.streaming.datamodel.Broadcast;
import com.example.android.streaming.datamodel.Channel;
import com.facebook.model.GraphUser;
import com.parse.ParseFile;
import com.parse.ParseUser;
import com.vg.hangwith.BuildConfig;
import com.vg.hangwith.R;

public class AvatarCache {
    private Map<String, LoadImageTask> tasks = new HashMap<String, AvatarCache.LoadImageTask>();
    private LruCache<String, RecyclingBitmapDrawable> memoryCache;
    public final static int AVATAR_BOUNDS = 100;

    private String cacheDir;
    private Context context;

    public synchronized void addTask(String tag, LoadImageTask task) {
        tasks.put(tag, task);
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "Added avatar load task for tag " + tag);
    }

    public synchronized void removeTask(String tag) {
        tasks.remove(tag);
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "Removed avatar load task for tag " + tag);
    }

    public synchronized void cancelTasks(int keepLastItems) {
        int count = 0;
        Iterator<Map.Entry<String, LoadImageTask>> iter = tasks.entrySet().iterator();
        while (iter.hasNext() && tasks.size() > keepLastItems) {
            Map.Entry<String, LoadImageTask> entry = iter.next();
            entry.getValue().cancel(true);
            iter.remove();
            count++;
        }
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "Canceled " + count + " avatar load tasks");
    }

    public void cancelTasks() {
        cancelTasks(0);
    }

    public final static Bitmap downscaleAvatar(Bitmap bitmap) {
        if (bitmap.getWidth() > AVATAR_BOUNDS && bitmap.getHeight() > AVATAR_BOUNDS) {
            int height = (int) Math.floor(bitmap.getHeight() / ((1.0f * bitmap.getWidth()) / AVATAR_BOUNDS));
            Bitmap scaled = Bitmap.createScaledBitmap(bitmap, AVATAR_BOUNDS, height, false);
            bitmap.recycle();
            bitmap = null;
            return scaled;
        } else {
            return bitmap;
        }
    }

    public final static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {
            // Calculate ratios of height and width to requested height and width
            final int heightRatio = Math.round((float) height / (float) reqHeight);
            final int widthRatio = Math.round((float) width / (float) reqWidth);

            // Choose the smallest ratio as inSampleSize value, this will guarantee
            // a final image with both dimensions larger than or equal to the
            // requested height and width.
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
        }

        return inSampleSize;
    }

    public class LoadImageTask extends AsyncTask<Void, Void, RecyclingBitmapDrawable> {
        protected RecyclingImageView image;
        protected String url, tag;
        protected boolean avatar;

        public LoadImageTask(String url, String tag, boolean avatar, RecyclingImageView image) {
            super();
            this.url = url;
            this.tag = tag;
            this.image = image;
            this.avatar = avatar;
            image.setTag(R.string.tag_key, tag);
            addTask(tag, this);
        }

        @Override
        protected RecyclingBitmapDrawable doInBackground(Void... dummy) {
            if (isCancelled() || !isSameImage())
                return null;
            RecyclingBitmapDrawable drawable = getAvatarFromMemCache(tag);
            if (drawable == null) {
                drawable = getAvatarFromDiskCache(tag);
                if (drawable == null) {
                    try {
                        if (BuildConfig.DEBUG)
                            Log.d(StreamingApp.TAG, "Loading avatar " + url);

                        /* First decode bounds to check the image size. */
                        BitmapFactory.Options options = new BitmapFactory.Options();

                        /* Calculate if the avatar should be down scaled. */
                        if (avatar) {
                            options.inJustDecodeBounds = true;
                            BitmapFactory.decodeStream(new URL(url).openConnection().getInputStream(), null, options);
                            options.inSampleSize = calculateInSampleSize(options, AVATAR_BOUNDS, AVATAR_BOUNDS);
                        }
                        options.inJustDecodeBounds = false;

                        /* Download down scaled avatar. */
                        Bitmap bitmap = BitmapFactory.decodeStream(new URL(url).openConnection().getInputStream(), null, options);
                        if (bitmap != null) {
                            drawable = new RecyclingBitmapDrawable(context.getResources(), bitmap);
                            if (drawable != null) {
                                addAvatarToDiskCache(tag, url, drawable);
                                addAvatarToMemoryCache(tag, drawable);
                            }
                        }
                    } catch (Exception e) {
                        Log.w(StreamingApp.TAG, "Failed to load and save avatar image. " + e.getMessage());
                    }
                } else {
                    addAvatarToMemoryCache(tag, drawable);
                }
            }
            return drawable;
        }

        private synchronized boolean isSameImage() {
            // In case that the same image is reused for different avatar (during scroll), this
            // function will return false.
            Object imageTag = image.getTag(R.string.tag_key);
            return imageTag != null && imageTag.equals(tag);
        }

        private void finishedWithResult(RecyclingBitmapDrawable result) {
            if (result != null && isSameImage())
                image.setImageDrawable(result);
            removeTask(tag);
        }

        @Override
        protected void onPostExecute(RecyclingBitmapDrawable result) {
            finishedWithResult(result);
            super.onPostExecute(result);
        }

        @Override
        protected void onCancelled(RecyclingBitmapDrawable result) {
            finishedWithResult(result);
            super.onCancelled();
        }

        @Override
        protected void onCancelled() {
            finishedWithResult(null);
            super.onCancelled();
        }
    }

    public AvatarCache(Context context) {
        super();

        // Get max available VM memory, exceeding this amount will throw an
        // OutOfMemory exception. Stored in kilobytes as LruCache takes an
        // int in its constructor.
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

        // Use 1/10th of the available memory for this memory cache. With small avatars like
        // we have this is enough to keep ~100 avatars in cache.
        final int cacheSize = maxMemory / 10;

        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "Init avatar cache, size: " + cacheSize + ", max mem size: " + maxMemory);

        memoryCache = new LruCache<String, RecyclingBitmapDrawable>(cacheSize) {
            @Override
            protected int sizeOf(String key, RecyclingBitmapDrawable drawable) {
                // The cache size will be measured in kilobytes rather than
                // number of items.
                Bitmap bitmap = drawable.getBitmap();
                int bitmapSize = bitmap != null ? bitmap.getByteCount() / 1024 : 0;
                return bitmapSize == 0 ? 1 : bitmapSize;
            }

            @Override
            protected void entryRemoved(boolean evicted, String key, RecyclingBitmapDrawable oldValue,
                    RecyclingBitmapDrawable newValue) {
                // The removed entry is a recycling drawable, so notify it.
                // that it has been removed from the memory cache
                oldValue.setIsCached(false);
            }
        };

        this.cacheDir = context.getCacheDir().getAbsolutePath();
        this.context = context;
    }

    public void flush() {
        int oldSize = memoryCache.size();
        memoryCache.evictAll();
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "Flush avatar cache, flushed " + (oldSize - memoryCache.size()) + " new size "
                    + memoryCache.size());
        cancelTasks();
    }

    public void addAvatarToMemoryCache(String key, RecyclingBitmapDrawable drawable) {
        if (getAvatarFromMemCache(key) == null) {
            drawable.setIsCached(true);
            memoryCache.put(key, drawable);
            if (BuildConfig.DEBUG)
                Log.d(StreamingApp.TAG, "Add to avatar cache, size: " + memoryCache.size());
        }
    }

    public RecyclingBitmapDrawable getAvatarFromMemCache(String key) {
        return memoryCache.get(key);
    }

    public void addAvatarToDiskCache(String name, String url, RecyclingBitmapDrawable drawable) throws IOException {
        if (drawable == null)
            return;
        File dir = new File(cacheDir);
        if (!dir.exists())
            dir.mkdirs();
        File file = new File(dir, name);
        Bitmap bitmap = drawable.getBitmap();
        if (!file.exists() && bitmap != null) {
            OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
            drawable.getBitmap().compress(Bitmap.CompressFormat.PNG, 85, out);
            out.flush();
            out.close();
        }
    }

    /*
     * Update avatar from the network if older than this.
     */
    public static final int AVATAR_MAX_AGE_DAYS = 7;

    public RecyclingBitmapDrawable getAvatarFromDiskCache(String name) {
        File file = new File(cacheDir, name);

        /* Check if cached bitmap is old. */
        if ((System.currentTimeMillis() - file.lastModified()) > AVATAR_MAX_AGE_DAYS * 24 * 60 * 60 * 1000)
            return null;
        try {
            Bitmap bitmap = BitmapFactory.decodeFile(file.getCanonicalPath());
            if (bitmap != null) {
                //                Log.w(App.TAG, "Loaded " + (bitmap.getByteCount() / 1024.0f) + "K bitmap " + name + " w: "
                //                        + bitmap.getWidth() + " h: " + bitmap.getHeight());
                return new RecyclingBitmapDrawable(context.getResources(), bitmap);
            }
        } catch (Exception e) {
            Log.w(StreamingApp.TAG, "Failed to decode avatar image " + name + ". " + e.getMessage());
        }
        return null;
    }

    public static boolean isValidURL(String url) {
        try {
            new URL(url);
            return true;
        } catch (Exception e) {
        }
        return false;
    }

    public void loadUrlAvatar(String url, String name, RecyclingImageView image, int placeholder, boolean checkDiskCache) {
        RecyclingBitmapDrawable drawable = getAvatarFromMemCache(name);
        if (drawable == null && checkDiskCache) {
            drawable = getAvatarFromDiskCache(name);
            if (drawable != null)
                addAvatarToMemoryCache(name, drawable);
        }
        if (drawable == null) {
            image.setImageResource(placeholder);
            if (url != null && isValidURL(url))
                new LoadImageTask(url, name, true, image).execute();
        } else {
            image.setImageDrawable(drawable);
        }
    }

    public static String getUserAvatarURL(ParseUser user) {
        if (user == null)
            return null;
        if (user.get("avatar") == null || user.get("avatar") == JSONObject.NULL)
            return user.getString("avatar_url");
        if (user.get("avatar") instanceof JSONObject)
            Log.w(StreamingApp.TAG, "JSONObject found instead of ParseFile: " + ((JSONObject) user.get("avatar")).toString());
        return ((ParseFile) user.get("avatar")).getUrl();
    }

    public static String getUserAvatarURL(GraphUser user) {
        return "http://graph.facebook.com/" + user.getId() + "/picture";
    }

    public static String getBroadcastAvatarURL(Broadcast broadcast) {
        if (broadcast.getThumbnail() == null)
            return null;
        return broadcast.getThumbnail().getUrl();
    }

    public void loadUserAvatar(ParseUser user, RecyclingImageView image, int placeholder, boolean checkDiskCache) {
        if (user != null)
            loadUrlAvatar(getUserAvatarURL(user), user.getUsername(), image, placeholder, checkDiskCache);
    }

    public void loadUserAvatar(GraphUser user, RecyclingImageView image, int placeholder, boolean checkDiskCache) {
        if (user != null)
            loadUrlAvatar(getUserAvatarURL(user), user.getId(), image, placeholder, checkDiskCache);
    }

    public void loadBroadcastAvatar(Broadcast broadcast, RecyclingImageView image, int placeholder,
            boolean checkDiskCache) {
        if (broadcast != null)
            loadUrlAvatar(getBroadcastAvatarURL(broadcast), broadcast.getObjectId(), image, placeholder, checkDiskCache);
    }

    public void clearUserAvatar(ParseUser user) {
        File file = new File(cacheDir, user.getUsername());
        if (file.exists())
            file.delete();
        memoryCache.remove(user.getUsername());
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "Remove avatar from cache, size: " + memoryCache.size());
    }

    public static String getChannelImageURL(Channel channel, boolean small, boolean ageRestricted) {
        if (ageRestricted) {
            if (small && channel.getSmallRestrictedState() != null)
                return channel.getSmallRestrictedState().getUrl();
            else if (!small && channel.getLargeRestrictedState() != null)
                return channel.getLargeRestrictedState().getUrl();
        } else {
            if (small && channel.getSmallEmptyState() != null)
                return channel.getSmallEmptyState().getUrl();
            else if (!small && channel.getLargeEmptyState() != null)
                return channel.getLargeEmptyState().getUrl();
        }
        return null;
    }

    public static final String channelImageCacheName(Channel channel, boolean small, boolean ageRestricted) {
        return channel.getObjectId() + "-" + (ageRestricted ? "age" : "empty") + "-" + (small ? "small" : "large");
    }

    public boolean loadChannelImage(Channel channel, RecyclingImageView image, boolean checkDiskCache, boolean small,
            boolean ageRestricted) {
        boolean result = false;

        if (channel == null)
            return false;

        String name = channelImageCacheName(channel, small, ageRestricted);
        RecyclingBitmapDrawable drawable = getAvatarFromMemCache(name);
        if (drawable == null && checkDiskCache) {
            drawable = getAvatarFromDiskCache(name);
            if (drawable != null)
                addAvatarToMemoryCache(name, drawable);
        }

        if (drawable == null) {
            String url = getChannelImageURL(channel, small, ageRestricted);
            result = url != null && isValidURL(url);
            if (result)
                new LoadImageTask(url, name, false, image).execute();
        } else {
            image.setImageDrawable(drawable);
            result = true;
        }
        return result;
    }

    public void loadUrlImage(String url, RecyclingImageView image, String name, boolean checkDiskCache) {
        RecyclingBitmapDrawable drawable = getAvatarFromMemCache(name);
        if (drawable == null && checkDiskCache) {
            drawable = getAvatarFromDiskCache(name);
            if (drawable != null)
                addAvatarToMemoryCache(name, drawable);
        }
        if (drawable == null) {
            if (url != null && isValidURL(url))
                new LoadImageTask(url, name, false, image).execute();
        } else {
            image.setImageDrawable(drawable);
        }
    }
}

注意,它在某些地方使用 Parse 框架。无视就好。

在本例中,AvatarCache 在 doInBackground() 函数中通过 url 加载图像。正如你所看到的,它得到了一个输出 url 的输入流。您可以对其进行修改以向其提供一些用于加载图像的不同输入流。然后还需要修改loadUrlImage()。换句话说,只需删除 url 的东西。

这就是您可以将它与 Uri 一起使用的方式。修改它以使用输入流或字节数组。只需使用适当的 BitmapFactory.decodeSomething() 方法即可。

public Bitmap getBitmap(Uri uri) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        AssetFileDescriptor fd = null;
        Bitmap b = null;
        try {
            fd = getContentResolver().openAssetFileDescriptor(uri, "r");
            if (fd != null) {
                options.inJustDecodeBounds = true;
                BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, options);
                options.inSampleSize = AvatarCache.calculateInSampleSize(options, AvatarCache.AVATAR_BOUNDS, AvatarCache.AVATAR_BOUNDS);
                options.inJustDecodeBounds = false;
                b = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, options);
                try {
                    fd.close();
                } catch (IOException e) {
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return b;

}

【讨论】:

    【解决方案2】:

    如您所见,AvatarCache 在此示例中仅静态使用。如果您需要管理大量图像,例如照片库 preview.create AvatarCache 在您的应用程序实例中。同时添加内存管理方法。

        @Override
        public void onCreate() {
            super.onCreate();
    
            avatarCache = new AvatarCache(this);
        }
    
       public void onTrimMemory(int level) {
            if (level == TRIM_MEMORY_COMPLETE || level == TRIM_MEMORY_RUNNING_CRITICAL || level == TRIM_MEMORY_RUNNING_LOW) {
                if (avatarCache != null)
                    avatarCache.flush();
            }
            super.onTrimMemory(level);
        }
    
        @Override
        public void onLowMemory() {
            Log.w(StreamingApp.TAG, "Low memory event received. Clear avatars cache.");
            if (avatarCache != null)
                avatarCache.flush();
            super.onLowMemory();
        }
    

    然后你可以像这样使用它:

    avatarCache.loadUserAvatar(...);
    

    它会自动加载图像并将其放置到缓存中。当应用内存不足时,缓存会被刷新。

    希望这会有所帮助。这里有很多东西,但是当你经历过一次时,你的 android 应用程序中的图像将永远不会出现问题。编码愉快!

    附言。如果需要,请提出问题。只需具体说明您需要询问的内容,并提供您特定用例的一些背景信息。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多