안 드 로 이 드 볼 리 라 이브 러 리 의 이미지 로드 기능 을 깊이 분석 하 다.
Volley 프레임 워 크 는 네트워크 그림 을 요청 하 는 데 도 많은 작업 을 했 고 여러 가지 방법 을 제공 했다.본 고 는 ImageLoader 를 사용 하여 네트워크 그림 을 불 러 오 는 것 을 소개 한다.
ImageLoader 의 내 부 는 ImageRequest 를 사용 하여 이 루어 집 니 다.구조 기 는 ImageCache 캐 시 형 삼 을 전송 하여 이미지 캐 시 기능 을 실현 할 수 있 으 며 중복 링크 를 걸 러 서 중복 전송 요청 을 피 할 수 있 습 니 다.
다음은 ImageLoader 에서 그림 을 불 러 오 는 실현 방법 입 니 다.
public void displayImg(View view){
ImageView imageView = (ImageView)this.findViewById(R.id.image_view);
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext());
ImageLoader imageLoader = new ImageLoader(mQueue, new BitmapCache());
ImageListener listener = ImageLoader.getImageListener(imageView,R.drawable.default_image, R.drawable.default_image);
imageLoader.get("http://developer.android.com/images/home/aw_dac.png", listener);
//
//imageLoader.get("http://developer.android.com/images/home/aw_dac.png",listener, 200, 200);
}
ImageLoader.getImageListener()방법 으로 ImageListener 인 스 턴 스 를 만 든 후,imageLoader.get()방법 에 이 모니터 와 그림 의 url 을 추가 하면 네트워크 그림 을 불 러 올 수 있 습 니 다.다음은 LruCache 로 구현 되 는 캐 시 클래스 입 니 다.
public class BitmapCache implements ImageCache {
private LruCache<String, Bitmap> cache;
public BitmapCache() {
cache = new LruCache<String, Bitmap>(8 * 1024 * 1024) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight();
}
};
}
@Override
public Bitmap getBitmap(String url) {
return cache.get(url);
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
cache.put(url, bitmap);
}
}
마지막 으로 AndroidManifest.xml 파일 에 네트워크 에 접근 할 수 있 는 권한 을 추가 하 는 것 을 잊 지 마 세 요.
<uses-permission android:name="android.permission.INTERNET"/>
2.소스 코드 분석(1)Volley 요청 대기 열 초기 화
mReqQueue = Volley.newRequestQueue(mCtx);
주로 이 줄 입 니 다.
#Volley
public static RequestQueue newRequestQueue(Context context) {
return newRequestQueue(context, null);
}
public static RequestQueue newRequestQueue(Context context, HttpStack stack)
{
return newRequestQueue(context, stack, -1);
}
public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
String userAgent = "volley/0";
try {
String packageName = context.getPackageName();
PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
userAgent = packageName + "/" + info.versionCode;
} catch (NameNotFoundException e) {
}
if (stack == null) {
if (Build.VERSION.SDK_INT >= 9) {
stack = new HurlStack();
} else {
// Prior to Gingerbread, HttpUrlConnection was unreliable.
// See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
}
}
Network network = new BasicNetwork(stack);
RequestQueue queue;
if (maxDiskCacheBytes <= -1)
{
// No maximum size specified
queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
}
else
{
// Disk cache size specified
queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network);
}
queue.start();
return queue;
}
여 기 는 주로 HttpStack 을 초기 화 하 는 것 입 니 다.HttpStack 은 API 가 9 보다 클 때 HttpUrl Connetcion 을 선택 하고 반대로 HttpClient 를 선택 합 니 다.여 기 는 Http 관련 코드 에 관심 이 없습니다.이 어 RequestQueue 를 초기 화하 고 start()방법 을 호출 했다.
다음은 RequestQueue 의 구 조 를 살 펴 보 겠 습 니 다.
public RequestQueue(Cache cache, Network network) {
this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
}
public RequestQueue(Cache cache, Network network, int threadPoolSize) {
this(cache, network, threadPoolSize,
new ExecutorDelivery(new Handler(Looper.getMainLooper())));
}
public RequestQueue(Cache cache, Network network, int threadPoolSize,
ResponseDelivery delivery) {
mCache = cache;
mNetwork = network;
mDispatchers = new NetworkDispatcher[threadPoolSize];
mDelivery = delivery;
}
초기 화 는 주로 4 개의 매개 변수:mCache,mNetwork,mDispatchers,mDelivery 입 니 다.첫 번 째 는 하 드 디스크 캐 시 입 니 다.두 번 째 는 Http 관련 작업 에 사 용 됩 니 다.세 번 째 는 퍼 가기 요청 에 사용 되 는 것 입 니 다.네 번 째 매개 변 수 는 결 과 를 UI 스 레 드 로 전송 하 는 데 사 용 됩 니 다(ps:new Handler(Looper.getMainLooper())를 볼 수 있 습 니 다.이제 start 방법 을 보 겠 습 니 다.
#RequestQueue
/**
* Starts the dispatchers in this queue.
*/
public void start() {
stop(); // Make sure any currently running dispatchers are stopped.
// Create the cache dispatcher and start it.
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
mCacheDispatcher.start();
// Create network dispatchers (and corresponding threads) up to the pool size.
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
mCache, mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
우선 stop 입 니 다.전송 기 가 종료 되 었 는 지 확인 하 십시오.사실은 내부 의 몇 개의 스 레 드 가 종료 되 었 습 니 다.여기 서 관심 이 있 으 면 소스 코드 를 볼 수 있 습 니 다.Volley 에서 스 레 드 가 종료 되 었 는 지 참고 하 십시오(몇 개의 스 레 드 는 while(true){/doSomething}).다음은 CacheDispatcher 를 초기 화하 고 start()를 호출 합 니 다.NetworkDispatcher 를 초기 화하 고 start()를 호출 합 니 다.
위의 트랜스 퍼 는 모두 스 레 드 입 니 다.볼 수 있 습 니 다.여기 몇 개의 스 레 드 가 우 리 를 도와 일 을 하고 있 습 니 다.구체 적 인 소스 코드 는 우리 가 잠시 후에 보고 있 습 니 다.
자,여기까지 볼 리 의 초기 화 에 관 한 코드 를 완 료 했 습 니 다.다음은 ImageLoader 관련 소스 코드 를 초기 화 하 는 것 을 보 겠 습 니 다.
(2)ImageLoader 초기 화
#VolleyHelper
mImageLoader = new ImageLoader(mReqQueue, new ImageCache()
{
private final LruCache<String, Bitmap> mLruCache = new LruCache<String, Bitmap>(
(int) (Runtime.getRuntime().maxMemory() / 10))
{
@Override
protected int sizeOf(String key, Bitmap value)
{
return value.getRowBytes() * value.getHeight();
}
};
@Override
public void putBitmap(String url, Bitmap bitmap)
{
mLruCache.put(url, bitmap);
}
@Override
public Bitmap getBitmap(String url)
{
return mLruCache.get(url);
}
});
#ImageLoader
public ImageLoader(RequestQueue queue, ImageCache imageCache) {
mRequestQueue = queue;
mCache = imageCache;
}
간단 합 니 다.우리 가 초기 화 한 RequestQueue 와 LruCache 에 따라 ImageLoader 를 초기 화 했 습 니 다.(3)그림 불 러 오기
우리 가 그림 을 불 러 올 때 호출 하 는 것 은:
# VolleyHelper
getInstance().getImageLoader().get(url, new ImageLoader.ImageListener());
다음은 get 방법:
#ImageLoader
public ImageContainer get(String requestUrl, final ImageListener listener) {
return get(requestUrl, listener, 0, 0);
}
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight) {
return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
}
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight, ScaleType scaleType) {
// only fulfill requests that were initiated from the main thread.
throwIfNotOnMainThread();
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
// Try to look up the request in the cache of remote images.
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
// Return the cached bitmap.
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
imageListener.onResponse(container, true);
return container;
}
// The bitmap did not exist in the cache, fetch it!
ImageContainer imageContainer =
new ImageContainer(null, requestUrl, cacheKey, imageListener);
// Update the caller to let them know that they should use the default bitmap.
imageListener.onResponse(imageContainer, true);
// Check to see if a request is already in-flight.
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) {
// If it is, add this request to the list of listeners.
request.addContainer(imageContainer);
return imageContainer;
}
// The request is not already in flight. Send the new request to the network and
// track it.
Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
cacheKey);
mRequestQueue.add(newRequest);
mInFlightRequests.put(cacheKey,
new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}
get 방법 을 볼 수 있 습 니 다.먼저 throw IfNotOnMainThread()방법 을 통 해 UI 스 레 드 에서 호출 해 야 합 니 다.그리고 들 어 오 는 매개 변수 에 따라 cacheKey 를 계산 하여 cache 를 가 져 옵 니 다.
=>cache 가 존재 하면 되 돌아 오 는 결 과 를 ImageContainer(cached Bitmap,requestUrl)로 봉 한 다음 imageListener.onResponse(container,true)로 직접 되 돌려 줍 니 다.우 리 는 그림 을 설정 할 수 있다.
=>cache 가 존재 하지 않 는 다 면 ImageContainer(bitmap 없 음)를 초기 화하 고,imageListener.onResponse(imageContainer,true)를 직접 리 셋 합 니 다.리 셋 에서 판단 할 수 있 도록 기본 그림 을 설정 합 니 다(그 러 니 여러분 이 listener 를 실현 할 때 resp.getBitmap()판단 하 는 것 을 잊 지 마 세 요!=null);
다음은 이 url 이 이미 요청 에 가 입 했 는 지 확인 합 니 다.이미 가입 했다 면,초기 화 된 ImageContainer 를 BatchedImageRequest 에 가입 하고 끝 을 되 돌려 줍 니 다.
새로운 요청 이 라면 MakeImageRequest 를 통 해 새로운 요청 을 만 든 다음 이 요청 을 각각 mRequestQueue 와 mInFlightRequests 에 추가 합 니 다.mInFlightRequests 에서 BatchedImageRequest 를 초기 화하 여 같은 요청 대기 열 을 저장 합 니 다.
여기 서 mRequestQueue 는 대상 이지 대기 열 데이터 구조 가 아니 므 로 add 방법 을 보 겠 습 니 다.
#RequestQueue
public <T> Request<T> add(Request<T> request) {
// Tag the request as belonging to this queue and add it to the set of current requests.
request.setRequestQueue(this);
synchronized (mCurrentRequests) {
mCurrentRequests.add(request);
}
// Process requests in the order they are added.
request.setSequence(getSequenceNumber());
request.addMarker("add-to-queue");
// If the request is uncacheable, skip the cache queue and go straight to the network.
if (!request.shouldCache()) {
mNetworkQueue.add(request);
return request;
}
// Insert request into stage if there's already a request with the same cache key in flight.
synchronized (mWaitingRequests) {
String cacheKey = request.getCacheKey();
if (mWaitingRequests.containsKey(cacheKey)) {
// There is already a request in flight. Queue up.
Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
if (stagedRequests == null) {
stagedRequests = new LinkedList<Request<?>>();
}
stagedRequests.add(request);
mWaitingRequests.put(cacheKey, stagedRequests);
if (VolleyLog.DEBUG) {
VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
}
} else {
// Insert 'null' queue for this cacheKey, indicating there is now a request in
// flight.
mWaitingRequests.put(cacheKey, null);
mCacheQueue.add(request);
}
return request;
}
}
우선 mCurrent Requests 가입 을 요청 합 니 다.이 mCurrent Requests 는 cancel 의 입 구 를 제공 하기 위해 처리 해 야 할 모든 Request 를 저장 합 니 다.이 요청 이 캐 시 되 지 않 으 면 mNetwork Queue 에 직접 가입 하고 돌아 갑 니 다.
그리고 이 요청 이 같은 요청 이 처리 되 고 있 는 지 판단 하고 있 으 면 mWaiting Requests 에 가입 합 니 다.하면,만약,만약...
mWaiting Requests.put(cacheKey,null)와 mCacheQueue.add(request)를 추가 합 니 다.
ok,여기 서 우 리 는 직관 적 인 코드 를 분석 하여 완 성 했 습 니 다.그러나 당신 은 도대체 어디에서 네트워크 요청 을 촉발 하고 그림 을 불 러 오 는 것 이 라 고 생각 할 수 있 습 니까?
그러면 먼저 우리 가 그림 을 불 러 올 때 MakeImageRequest 를 한 다음 에 이 요청 을 여러 대기 열 에 추가 합 니 다.주로 mCurrent Requests,mCacheQueue 를 포함 합 니 다.
그리고 우리 가 RequestQueue 를 초기 화 할 때 몇 개의 퍼 가기 스 레 드 를 시 작 했 는 지 기억 하 십 니까?CacheDispatcher 와 NetworkDispatcher.
사실은 네트워크 요청 은 이 몇 개의 스 레 드 에서 진정 으로 불 러 오 는 것 입 니 다.우 리 는 각각 보 겠 습 니 다.
(4)CacheDispatcher
구조 방법 보기;
#CacheDispatcher
public CacheDispatcher(
BlockingQueue<Request<?>> cacheQueue, BlockingQueue<Request<?>> networkQueue,
Cache cache, ResponseDelivery delivery) {
mCacheQueue = cacheQueue;
mNetworkQueue = networkQueue;
mCache = cache;
mDelivery = delivery;
}
이것 은 스 레 드 입 니 다.그러면 주요 코드 는 run 안에 있 을 것 입 니 다.
#CacheDispatcher
@Override
public void run() {
if (DEBUG) VolleyLog.v("start new dispatcher");
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// Make a blocking call to initialize the cache.
mCache.initialize();
while (true) {
try {
// Get a request from the cache triage queue, blocking until
// at least one is available.
final Request<?> request = mCacheQueue.take();
request.addMarker("cache-queue-take");
// If the request has been canceled, don't bother dispatching it.
if (request.isCanceled()) {
request.finish("cache-discard-canceled");
continue;
}
// Attempt to retrieve this item from cache.
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
request.addMarker("cache-miss");
// Cache miss; send off to the network dispatcher.
mNetworkQueue.put(request);
continue;
}
// If it is completely expired, just send it to the network.
if (entry.isExpired()) {
request.addMarker("cache-hit-expired");
request.setCacheEntry(entry);
mNetworkQueue.put(request);
continue;
}
// We have a cache hit; parse its data for delivery back to the request.
request.addMarker("cache-hit");
Response<?> response = request.parseNetworkResponse(
new NetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");
if (!entry.refreshNeeded()) {
// Completely unexpired cache hit. Just deliver the response.
mDelivery.postResponse(request, response);
} else {
// Soft-expired cache hit. We can deliver the cached response,
// but we need to also send the request to the network for
// refreshing.
request.addMarker("cache-hit-refresh-needed");
request.setCacheEntry(entry);
// Mark the response as intermediate.
response.intermediate = true;
// Post the intermediate response back to the user and have
// the delivery then forward the request along to the network.
mDelivery.postResponse(request, response, new Runnable() {
@Override
public void run() {
try {
mNetworkQueue.put(request);
} catch (InterruptedException e) {
// Not much we can do about this.
}
}
});
}
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
return;
}
continue;
}
}
}
ok,우선 이 캐 시 는 하 드 디스크 캐 시(디 렉 터 리 는 context.getCacheDir()/volley)를 말 합 니 다.메모리 캐 시 는 ImageLoader 에서 이미 판단 되 었 습 니 다.여 기 는 무한 순환 입 니 다.mCacheQueue 에서 요청 을 계속 꺼 내 고 요청 이 취소 되면 바로 끝 납 니 다.
다음 캐 시 에서 가 져 오기:
=>찾 지 못 하면 mNetworkQueue 가입
=>캐 시가 만 료 되면 mNetworkQueue 가입
그렇지 않 으 면 사용 가능 한 캐 시 를 찾 은 것 입 니 다.request.parseNetworkResponse 를 호출 하여 캐 시 에서 꺼 낸 data 와 responseHeaders 를 분석 합 니 다.다음은 TTL(주로 만 료 여 부 를 판단 합 니 다)을 판단 하고 만 료 되 지 않 으 면 mDelivery.post Response 를 통 해 직접 전달 한 다음 UI 스 레 드 로 되 돌려 줍 니 다.ttl 이 합 법 적 이지 않 으 면 리 셋 이 완료 되면 이 요청 을 mNetworkQueue 에 추가 합 니 다.
자,여 기 는 합 법 적 인 캐 시 를 받 으 면 UI 스 레 드 로 직접 전송 하 는 것 입 니 다.반대로 NetworkQueue 에 가입 합 니 다.
다음은 네트워크 디 스 패 치 를 보 겠 습 니 다.
(5)NetworkDispatcher
CacheDispatcher 와 유사 하고 스 레 드 이 며 핵심 코드 는 run 에 있 습 니 다.
# NetworkDispatcher
//new NetworkDispatcher(mNetworkQueue, mNetwork,mCache, mDelivery)
public NetworkDispatcher(BlockingQueue<Request<?>> queue,
Network network, Cache cache,
ResponseDelivery delivery) {
mQueue = queue;
mNetwork = network;
mCache = cache;
mDelivery = delivery;
}
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
while (true) {
long startTimeMs = SystemClock.elapsedRealtime();
Request<?> request;
try {
// Take a request from the queue.
request = mQueue.take();
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
return;
}
continue;
}
try {
request.addMarker("network-queue-take");
// If the request was cancelled already, do not perform the
// network request.
if (request.isCanceled()) {
request.finish("network-discard-cancelled");
continue;
}
addTrafficStatsTag(request);
// Perform the network request.
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-complete");
// If the server returned 304 AND we delivered a response already,
// we're done -- don't deliver a second identical response.
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
request.finish("not-modified");
continue;
}
// Parse the response here on the worker thread.
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");
// Write to cache if applicable.
// TODO: Only update cache metadata instead of entire record for 304s.
if (request.shouldCache() && response.cacheEntry != null) {
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}
// Post the response back.
request.markDelivered();
mDelivery.postResponse(request, response);
} catch (VolleyError volleyError) {
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
parseAndDeliverNetworkError(request, volleyError);
} catch (Exception e) {
VolleyLog.e(e, "Unhandled exception %s", e.toString());
VolleyError volleyError = new VolleyError(e);
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
mDelivery.postError(request, volleyError);
}
}
}
코드 를 보기 전에 우 리 는 먼저 논 리 를 생각해 보 겠 습 니 다.정상 적 인 상황 에서 우 리 는 요청 을 꺼 내 network 로 하여 금 우리 의 요청 을 처리 하 게 할 것 입 니 다.처리 가 끝 난 후에 캐 시 를 넣 고 퍼 가기 할 것 입 니 다.그럼 옳 고 그 름 을 봅 시다.
먼저 요청 꺼 내기;그리고 mNetwork.performRequest(request)를 통 해 우리 의 요청 을 처리 하고 NetworkResponse 를 받 습 니 다.다음은 request 를 사용 하여 우리 의 NetworkResponse 를 분석 합 니 다.
Response 를 받 은 후 캐 시 여 부 를 판단 하고 필요 하 다 면 캐 시 합 니 다.
마지막 mDelivery.postResponse(요청,응답);전달 하 다
ok,우리 의 기대 와 차이 가 많 지 않 습 니 다.
이렇게 되면 우리 볼 리 는 그림 을 불 러 오 는 핵심 논 리 를 분석 하고 간단하게 요약 한다.
먼저 RequestQueue 를 초기 화 합 니 다.주로 Dispatcher 스 레 드 를 몇 개 켜 는 것 입 니 다.스 레 드 는 요청 을 계속 읽 습 니 다(사용 하 는 차단 대기 열,메시지 가 없 으 면 차단)
우리 가 요청 을 한 후에 url,ImageView 속성 등에 따라 cacheCey 를 구성 한 다음 에 먼저 LruCache 에서 가 져 옵 니 다(이 캐 시 는 우리 가 구축 한 것 으로 ImageCache 인 터 페 이 스 를 실현 하 는 것 은 모두 합 법 적 입 니 다).가 져 오지 않 으 면 하 드 디스크 캐 시가 있 는 지 판단 합 니 다.이 단 계 는 getCacheDir 에서 가 져 옵 니 다(기본 5M).찾 지 못 하면 네트워크 에서 요청 합 니 다.
그러나 볼 리 의 그림 로드 를 발견 할 수 있 습 니 다.LIFO 라 는 전략 은 없습니다.그림 을 다운로드 하 는 것 도 메모리 에 완전히 추가 한 다음 에 압축 하 는 것 같 습 니 다.이렇게 보면 큰 그림,큰 파일 같은 것 은 폐 기 됩 니 다.
보기 에는 간단 해 보이 지만 보고 나 면 어떻게 하면 더 좋 을 때 이 라 이브 러 리 와 이미지 로 딩 라 이브 러 리 를 디자인 하 는 지 에 큰 도움 이 됩 니 다.
만약 에 관심 이 있다 면 여러분 은 소스 코드 분석 을 보 는 동시에 특정한 세부 적 인 실현 도 생각해 보 실 수 있 습 니 다.예 를 들 어:
Dispatcher 는 무한 순환 스 레 드 로 볼 리 가 어떻게 닫 혔 는 지 확인 할 수 있 습 니 다.
그림 압축 코드 는 ImageRequest 의 parseNetworkResponse 에서 어떻게 압축 되 었 는 지 볼 수 있 습 니 다.
so on…
마지막 으로 대략적인 흐름 도 를 붙 여서 기억 하기 편 하 게 한다.
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
Bitrise에서 배포 어플리케이션 설정 테스트하기이 글은 Bitrise 광고 달력의 23일째 글입니다. 자체 또는 당사 등에서 Bitrise 구축 서비스를 사용합니다. 그나저나 며칠 전 Bitrise User Group Meetup #3에서 아래 슬라이드를 발표했...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.