English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Gituhb 프로젝트
Volley 소스 코드 중국어 주석 프로젝트는 github에 업로드되었습니다. fork와 star를 환영합니다.
이 글을 쓰는 이유
본래 글은 github에서 유지되었지만, ImageLoader 소스 코드 분석 중 문제를 발견하여 도움을 부탁드립니다.
Volley 네트워크 이미지 가져오기
본래 Universal Image Loader의 소스 코드를 분석하려고 했지만, Volley가 이미 네트워크 이미지 로드 기능을 구현했음을 발견했습니다. 사실, 네트워크 이미지 로드는 여러 단계로 구성됩니다:
1. 네트워크 이미지의 URL을 가져옵니다.
2. 해당 URL에 대한 이미지가 로컬 캐시에 있는지 확인.
3. 로컬 캐시가 있으면 로컬 캐시 이미지를 사용하여 비동기回调을 통해 ImageView에 설정.
4. 로컬 캐시가 없으면 먼저 네트워크에서 가져와 로컬에 저장한 후, 비동기回调을 통해 ImageView에 설정.
Volley 소스 코드를 통해 Volley가 네트워크 이미지 로드를 이 단계로 구현하는지 확인해 보겠습니다.
ImageRequest.java
Volley의 아키텍처에 따라, 먼저 네트워크 이미지 요청을 생성해야 합니다. Volley는 ImageRequest 클래스를 포장하여 제공합니다. 그의 구체적인 구현을 보겠습니다:
/** 네트워크 이미지 요청 클래스. */ @SuppressWarnings("unused") public class ImageRequest extends Request<Bitmap> { /** 기본 이미지 가져오기 타임아웃 시간(단위: 밀리초) */ public static final int DEFAULT_IMAGE_REQUEST_MS = 1000; /** 기본 이미지 가져오기 재시도 횟수. */ public static final int DEFAULT_IMAGE_MAX_RETRIES = 2; private final Response.Listener<Bitmap> mListener; private final Bitmap.Config mDecodeConfig; private final int mMaxWidth; private final int mMaxHeight; private ImageView.ScaleType mScaleType; /** Bitmap 해석 동기화 락, 동시에 하나의 Bitmap이 메모리에 로드되고 해석되도록 보장하여 OOM을 방지합니다. */ private static final Object sDecodeLock = new Object(); /** * 네트워크 이미지 요청을 생성합니다. * @param url 이미지의 URL 주소. * @param listener 요청 성공 시 사용자가 설정한 콜백 인터페이스. * @param maxWidth 이미지의 최대 너비. * @param maxHeight 이미지의 최대 높이. * @param scaleType 이미지 확대 타입. * @param decodeConfig bitmap을 해석하는 구성. * @param errorListener 요청 실패 시 사용자가 설정한 콜백 인터페이스. */ public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight, ImageView.ScaleType scaleType, Bitmap.Config decodeConfig, Response.ErrorListener errorListener) { super(Method.GET, url, errorListener); mListener = listener; mDecodeConfig = decodeConfig; mMaxWidth = maxWidth; mMaxHeight = maxHeight; mScaleType = scaleType; } /** 네트워크 이미지 요청의 우선순위를 설정합니다. */ @Override public Priority getPriority() { return Priority.LOW; } @Override protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) { synchronized (sDecodeLock) { try { return doParse(response); } catch (OutOfMemoryError e) { return Response.error(new VolleyError(e)); } } } private Response<Bitmap> doParse(NetworkResponse response) { byte[] data = response.data; BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); Bitmap bitmap; if (mMaxWidth == 0 && mMaxHeight == 0) { decodeOptions.inPreferredConfig = mDecodeConfig; bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); } else { // 실제 네트워크 이미지의 크기를 가져오기. decodeOptions.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); int actualWidth = decodeOptions.outWidth; int actualHeight = decodeOptions.outHeight; int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight, actualWidth, actualHeight, mScaleType); int desireHeight = getResizedDimension(mMaxWidth, mMaxHeight, actualWidth, actualHeight, mScaleType); decodeOptions.inJustDecodeBounds = false; decodeOptions.inSampleSize = findBestSampleSize(actualWidth, actualHeight, desiredWidth, desireHeight);}} Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth || tempBitmap.getHeight() > desireHeight)) { bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desireHeight, true); tempBitmap.recycle(); } else { bitmap = tempBitmap; } } if (bitmap == null) { return Response.error(new VolleyError(response)); } else { return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response)); } } static int findBestSampleSize( int actualWidth, int actualHeight, int desiredWidth, int desireHeight) { double wr = (double) actualWidth / desiredWidth; double hr = (double) actualHeight / desireHeight; double ratio = Math.min(wr, hr); float n = 1.0f; while ((n * 2) <= ratio) { n *= 2; } return (int) n; } /** 이미지뷰의 ScaleType에 따라 이미지 크기를 설정합니다. */ private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, int actualSecondary, ImageView.ScaleType scaleType) {}} // ImageView의 최대값이 설정되지 않았다면, 네트워크 이미지의 실제 크기를 직접 반환합니다. if ((maxPrimary == 0) && (maxSecondary == 0)) { return actualPrimary; } // ImageView의 ScaleType이 FIX_XY라면, 그 값을 이미지 최대값으로 설정합니다. if (scaleType == ImageView.ScaleType.FIT_XY) { if (maxPrimary == 0) { return actualPrimary; } return maxPrimary; } if (maxPrimary == 0) { double ratio = (double)maxSecondary / (double)actualSecondary; return (int)(actualPrimary * ratio); } if (maxSecondary == 0) { return maxPrimary; } double ratio = (double) actualSecondary / (double) actualPrimary; int resized = maxPrimary; if (scaleType == ImageView.ScaleType.CENTER_CROP) { if ((resized * ratio) < maxSecondary) { resized = (int)(maxSecondary / ratio); } return resized; } if ((resized * ratio) > maxSecondary) { resized = (int)(maxSecondary / ratio); } return resized; } @Override protected void deliverResponse(Bitmap response) { mListener.onResponse(response); } }
Volley 자체 프레임워크가 네트워크 요청의 로컬 캐시를 구현했기 때문에, ImageRequest이 주로 하는 일은 바이트 스트림을 Bitmap으로 변환하고, 변환 과정에서 정적 변수를 사용하여 한 번에 하나의 Bitmap만 변환하여 OOM을 방지하고, ScaleType과 사용자가 설정한 MaxWidth와 MaxHeight를 사용하여 이미지 크기를 설정합니다.
종합적으로 말해, ImageRequest의 구현은 매우 간단합니다. 여기서는 더 이상 설명하지 않겠습니다. ImageRequest의 단점은 다음과 같습니다:
1. 사용자가 많은 설정을 해야 합니다. 이미지의 크기의 최대값을 포함하여.
2. 이미지의 메모리 캐시가 없습니다. Volley의 캐시는 디스크 기반의 캐시이며, 객체 반시리얼라이제이션 과정이 있습니다.
ImageLoader.java
위 두 가지 단점을 해결하기 위해 Volley는 더 강력한 ImageLoader 클래스를 제공했습니다. 중요한 것은 메모리 캐시가 추가되었습니다.
ImageLoader의 소스코드를 설명하기 전에, ImageLoader의 사용법을 먼저 설명해야 합니다. 이전의 Request 요청과 달리, ImageLoader는 new로 생성된 후 RequestQueue에 스케줄링하는 것이 아니라, 사용법은 주로 다음과 같습니다.4보:
• RequestQueue 객체를 생성합니다.
RequestQueue queue = Volley.newRequestQueue(context);
• ImageLoader 객체를 생성합니다.
ImageLoader 생성자는 두 개의 매개변수를 받습니다. 첫 번째는 RequestQueue 객체이며, 두 번째는 ImageCache 객체입니다(즉, 메모리 캐시 클래스로, 구체적인 구현은 나중에 제공하겠습니다. ImageLoader 소스코드를 설명한 후, LRU 알고리즘을 사용하는 ImageCache 구현 클래스를 제공하겠습니다.).
ImageLoader imageLoader = new ImageLoader(queue, new ImageCache() { @Override public void putBitmap(String url, Bitmap bitmap) {} @Override public Bitmap getBitmap(String url) { return null; } });
• ImageListener 객체를 얻습니다.
ImageListener listener = ImageLoader.getImageListener(imageView, R.drawable.default_imgage, R.drawable.failed_image);
• ImageLoader의 get 메서드를 사용하여 네트워크 이미지를 로드합니다.
imageLoader.get(mImageUrl, listener, maxWidth, maxHeight, scaleType);
ImageLoader의 사용법을 알아보았다면, 사용법과 함께 ImageLoader의 소스코드를 살펴보겠습니다:
@SuppressWarnings({"unused", "StringBufferReplaceableByString"}) public class ImageLoader { /** * ImageLoader를 호출하기 위한 RequestQueue와 연결합니다. */ private final RequestQueue mRequestQueue; /** 이미지 메모리 캐시 인터페이스 구현 클래스. */ private final ImageCache mCache; /** 동시에 실행되는 동일한 CacheKey의 BatchedImageRequest 집합을 저장합니다. */ private final HashMap<String, BatchedImageRequest> mInFlightRequests = new HashMap<String, BatchedImageRequest>(); private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<String, BatchedImageRequest>(); /** 메인 스레드의 Handler를 가져옵니다. */ private final Handler mHandler = new Handler(Looper.getMainLooper()); private Runnable mRunnable; /** 이미지 K1캐시 인터페이스, 이미지의 메모리 캐시 작업을 사용자가 구현하도록 합니다. */ public interface ImageCache { Bitmap getBitmap(String url); void putBitmap(String url, Bitmap bitmap); } /** ImageLoader를 생성합니다. */ public ImageLoader(RequestQueue queue, ImageCache imageCache) { mRequestQueue = queue; mCache = imageCache; } /** 생성자를 통해 네트워크 이미지 요청 성공과 실패의 콜백 인터페이스를 정의합니다. */ public static ImageListener getImageListener(final ImageView view, final int defaultImageResId, final int errorImageResId) { return new ImageListener() { @Override public void onResponse(ImageContainer response, boolean isImmediate) { if (response.getBitmap() != null) { view.setImageBitmap(response.getBitmap()); } else if (defaultImageResId != 0) { view.setImageResource(defaultImageResId); } } @Override public void onErrorResponse(VolleyError error) { if (errorImageResId != 0) { view.setImageResource(errorImageResId); } } }; } public ImageContainer get(String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight, ScaleType scaleType) { // 현재 메서드가 UI 스레드에서 실행되는지 확인합니다. 아닌 경우 예외를 터립니다. throwIfNotOnMainThread(); final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); // 從L1키에 따라 레벨 캐시에서 Bitmap을 가져옵니다. Bitmap cacheBitmap = mCache.getBitmap(cacheKey); if (cacheBitmap != null) { // L1캐시 히트 시, 캐시 히트된 Bitmap을 통해 ImageContainer를 구성하고 imageListener의 응답 성공 인터페이스를 호출합니다. ImageContainer container = new ImageContainer(cacheBitmap, requestUrl, null, null); // 주의: 현재 UI 스레드에서 실행 중이므로, 여기서 onResponse 메서드를 호출하고 있으며, 콜백이 아닙니다. imageListener.onResponse(container, true); return container; } ImageContainer imageContainer = new ImageContainer(null, requestUrl, cacheKey, imageListener); // L1캐시 히트 실패 시, 먼저 ImageView에 기본 이미지를 설정해야 합니다. 그런 다음 서브 스레드를 통해 네트워크 이미지를 끌어오고 표시합니다. imageListener.onResponse(imageContainer, true); // cacheKey에 해당하는 ImageRequest 요청이 실행 중인지 확인합니다. BatchedImageRequest request = mInFlightRequests.get(cacheKey); if (request != null) { // 동일한 ImageRequest가 이미 실행 중이면, 동일한 ImageRequest를 동시에 실행할 필요가 없습니다. // 그런 다음 BatchedImageRequest의 mContainers 집합에 해당하는 ImageContainer를 추가합니다. // 현재 실행 중인 ImageRequest가 끝나면, 얼마나 많은 대기 중인 ImageRequest가 있는지 확인합니다 // 그런 다음 mContainers 집합에 대해 콜백을 수행합니다. request.addContainer(imageContainer); return imageContainer; } // L1캐시에 맞지 않으면, ImageRequest를 생성하고 RequestQueue의 스케줄링을 통해 네트워크 이미지를 가져옵니다 // 가져오는 방법은 다음과 같을 수 있습니다:2缓存(참고: 디스크 캐시) 또는 HTTP 네트워크 요청. Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey); mRequestQueue.add(newRequest); mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer)); return imageContainer; } /** 构造L1缓存的key值. */ private String getCacheKey(String url, int maxWidth, int maxHeight, ScaleType scaleType) { return new StringBuilder(url.length()) + 12).append("#W").append(maxWidth) .append("#H").append(maxHeight).append("#S").append(scaleType.ordinal()).append(url) .toString(); } public boolean isCached(String requestUrl, int maxWidth, int maxHeight) { return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE); } private boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) { throwIfNotOnMainThread(); String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); return mCache.getBitmap(cacheKey) != null; } /** 当L1缓存没有命中时,构造ImageRequest,通过ImageRequest和RequestQueue获取图片. */ protected Request<Bitmap> makeImageRequest(final String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType, final String cacheKey) { return new ImageRequest(requestUrl, new Response.Listener<Bitmap>() { @Override public void onResponse(Bitmap response) { onGetImageSuccess(cacheKey, response); } }, maxWidth, maxHeight, scaleType, Bitmap.Config.RGB_565, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { onGetImageError(cacheKey, error); } }); } /** 이미지 요청 실패 콜백. UI 스레드에서 실행됩니다. */ private void onGetImageError(String cacheKey, VolleyError error) { BatchedImageRequest request = mInFlightRequests.remove(cacheKey); if (request != null) { request.setError(error); batchResponse(cacheKey, request); } } /** 이미지 요청 성공 콜백. UI 스레드에서 실행됩니다. */ protected void onGetImageSuccess(String cacheKey, Bitmap response) { // 增加L1缓存的键值对. mCache.putBitmap(cacheKey, response); // 처음으로 ImageRequest가 성공적으로 실행된 시점에서, 이 기간 동안 차단된 같은 ImageRequest에 대한 성공回调 인터페이스를 호출합니다. BatchedImageRequest request = mInFlightRequests.remove(cacheKey); if (request != null) { request.mResponseBitmap = response; // 차단된 ImageRequest에 대한 결과를 분배합니다. batchResponse(cacheKey, request); } } private void batchResponse(String cacheKey, BatchedImageRequest request) { mBatchedResponses.put(cacheKey, request); if (mRunnable == null) { mRunnable = new Runnable() { @Override public void run() { for (BatchedImageRequest bir : mBatchedResponses.values()) { for (ImageContainer container : bir.mContainers) { if (container.mListener == null) { continue; } if (bir.getError() == null) { container.mBitmap = bir.mResponseBitmap; container.mListener.onResponse(container, false); } else { container.mListener.onErrorResponse(bir.getError()); } } } mBatchedResponses.clear(); mRunnable = null; } }; // Post the runnable mHandler.postDelayed(mRunnable, 100); } } private void throwIfNotOnMainThread() { if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException("ImageLoader는 메인 스레드에서 호출되어야 합니다."); } } /** 요청 성공과 실패의 콜백 인터페이스를 추출합니다. 기본적으로 Volley가 제공하는 ImageListener를 사용할 수 있습니다. */ public interface ImageListener extends Response.ErrorListener { void onResponse(ImageContainer response, boolean isImmediate); } /** 네트워크 이미지 요청의 캐리어 객체. */ public class ImageContainer { /** ImageView가 로드해야 하는 Bitmap. */ private Bitmap mBitmap; /** L1缓存的key */ private final String mCacheKey; /** ImageRequest 요청의 URL. */ private final String mRequestUrl; /** 이미지 요청 성공 또는 실패의 콜백 인터페이스 클래스. */ private final ImageListener mListener; public ImageContainer(Bitmap bitmap, String requestUrl, String cacheKey, ImageListener listener) { mBitmap = bitmap; mRequestUrl = requestUrl; mCacheKey = cacheKey; mListener = listener; } public void 취소요청() { if (mListener == null) { return; } BatchedImageRequest request = mInFlightRequests.get(mCacheKey); if (request != null) { boolean canceled = request.removeContainerAndCancelIfNecessary(this); if (canceled) { mInFlightRequests.remove(mCacheKey); } } else { request = mBatchedResponses.get(mCacheKey); if (request != null) { request.removeContainerAndCancelIfNecessary(this); if (request.mContainers.size() == 0) { mBatchedResponses.remove(mCacheKey); } } } } public Bitmap getBitmap() { return mBitmap; } public String getRequestUrl() { return mRequestUrl; } } /** * CacheKey가 같은 ImageRequest 요청의 추상 클래스. * 두 개의 ImageRequest이 같다고 판단하는 기준은 포함됩니다: * 1. url이 같습니다. * 2. maxWidth와 maxHeight가 같습니다. * 3. 표시되는 scaleType가 같습니다. * 동일한 CacheKey의 ImageRequest 요청은 동시에 여러 개가 있을 수 있지만, 반환할 Bitmap이 모두 같기 때문에 BatchedImageRequest를 사용합니다. * 이 기능을 구현하기 위해. 동일한 CacheKey의 ImageRequest은 동시에 하나만 있습니다. * 왜 RequestQueue의 mWaitingRequestQueue를 통해 이 기능을 구현하지 않는지?63; * 답변: 두 개의 ImageRequest이 같다고 판단할 수 없는 이유는 URL만으로는 아닙니다. */ private class BatchedImageRequest { /** 대응하는 ImageRequest 요청. */ private final Request<?> mRequest; /** 요청 결과의 Bitmap 객체. */ private Bitmap mResponseBitmap; /** ImageRequest의 오류. */ private VolleyError mError; /** 모든 같은 ImageRequest 요청 결과를 포장한 집합. */ private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>(); public BatchedImageRequest(Request<63;> request, ImageContainer container) { mRequest = request; mContainers.add(container); } public VolleyError getError() { return mError; } public void setError(VolleyError error) { mError = error; } public void addContainer(ImageContainer container) { mContainers.add(container); } public boolean removeContainerAndCancelIfNecessary(ImageContainer container) { mContainers.remove(container); if (mContainers.size() == 0) { mRequest.cancel(); return true; } return false; } } }
중요한 의문
Imageloader의 소스 코드에 대해 두 가지 중요한 의문이 있습니다?
•batchResponse 메서드의 구현.
저는 ImageLoader 클래스 안에 BatchedImageRequest 집합을 저장하는 HashMap이 왜 필요한지 궁금합니다?
private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<String, BatchedImageRequest>();
결국 batchResponse는 특정 ImageRequest가 성공적으로 호출된 뒤에 호출되는 콜백입니다. 호출 코드는 다음과 같습니다:
protected void onGetImageSuccess(String cacheKey, Bitmap response) { // 增加L1缓存的键值对. mCache.putBitmap(cacheKey, response); // 처음으로 ImageRequest가 성공적으로 실행된 시점에서, 이 기간 동안 차단된 같은 ImageRequest에 대한 성공回调 인터페이스를 호출합니다. BatchedImageRequest request = mInFlightRequests.remove(cacheKey); if (request != null) { request.mResponseBitmap = response; // 차단된 ImageRequest에 대한 결과를 분배합니다. batchResponse(cacheKey, request); } }
위 코드에서 볼 수 있듯이, ImageRequest가 성공적으로 요청된 후, mInFlightRequests에서 해당 BatchedImageRequest 객체를 가져옵니다. 동시에 방해받지 않고 같은 ImageRequest가 차단된 ImageContainer는 BatchedImageRequest의 mContainers 집합에 있습니다.
따라서, batchResponse 메서드는 BatchedImageRequest의 mContainers 집합을 탐색하는 것만으로도 충분하다고 생각합니다.
但是,ImageLoader 소스 코드에서, 추가적으로 필요하지 않다고 생각하는 HashMap 객체 mBatchedResponses를 생성하여 BatchedImageRequest 집합을 저장하고, batchResponse 메서드에서 집합에 대해 두 루프로 여러 번 탐색하는 것이 매우 이상합니다. 가르침을 요청드립니다.
诡异代码如下:
private void batchResponse(String cacheKey, BatchedImageRequest request) { mBatchedResponses.put(cacheKey, request); if (mRunnable == null) { mRunnable = new Runnable() { @Override public void run() { for (BatchedImageRequest bir : mBatchedResponses.values()) { for (ImageContainer container : bir.mContainers) { if (container.mListener == null) { continue; } if (bir.getError() == null) { container.mBitmap = bir.mResponseBitmap; container.mListener.onResponse(container, false); } else { container.mListener.onErrorResponse(bir.getError()); } } } mBatchedResponses.clear(); mRunnable = null; } }; // Post the runnable mHandler.postDelayed(mRunnable, 100); } }
我认为的代码实现应该是:
private void batchResponse(String cacheKey, BatchedImageRequest request) { if (mRunnable == null) { mRunnable = new Runnable() { @Override public void run() { for (ImageContainer container : request.mContainers) { if (container.mListener == null) { continue; } if (request.getError() == null) { container.mBitmap = request.mResponseBitmap; container.mListener.onResponse(container, false); } else { container.mListener.onErrorResponse(request.getError()); } } mRunnable = null; } }; // Post the runnable mHandler.postDelayed(mRunnable, 100); } }
•使用ImageLoader默认提供的ImageListener,我认为存在一个缺陷,即图片闪现问题.当为ListView的item设置图片时,需要增加TAG判断.因为对应的ImageView可能已经被回收利用了.
自定义L1缓存类
首先说明一下,所谓的L1和L2缓存分别指的是内存缓存和硬盘缓存.
实现L1缓存,我们可以使用Android提供的Lru缓存类,示例代码如下:
import android.graphics.Bitmap; import android.support.v4.util.LruCache; /** Lru算法的L1缓存实现类. */ @SuppressWarnings("unused") public class ImageLruCache implements ImageLoader.ImageCache { private LruCache<String, Bitmap> mLruCache; public ImageLruCache() { this((int) Runtime.getRuntime().maxMemory() / 8); } public ImageLruCache(final int cacheSize) { createLruCache(cacheSize); } private void createLruCache(final int cacheSize) { mLruCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes(); * value.getHeight(); } }; } @Override public Bitmap getBitmap(String url) { return mLruCache.get(url); } @Override public void putBitmap(String url, Bitmap bitmap) { mLruCache.put(url, bitmap); } }
이것이 이 문서의 모든 내용입니다. 많은 도움이 되길 바라며, 모두가呐喊 교육을 많이 지지해 주시길 바랍니다.
선언: 이 문서의 내용은 인터넷에서 가져왔으며, 저작권은 원저자에게 있으며, 인터넷 사용자가 자발적으로 기여하고 업로드한 내용입니다. 이 사이트는 소유권을 가지지 않으며, 인공 편집을 하지 않았으며, 관련 법적 책임도 부담하지 않습니다. 저작권 위반이 의심되는 내용을 발견하면 notice#w로 이메일을 보내 주시기 바랍니다.3codebox.com(메일을 보내는 경우, #을 @으로 변경하십시오.)을 통해 신고하시고 관련 증거를 제공하시면, 사실이 확인되면 이 사이트는 즉시 저작권 위반 내용을 삭제합니다.