[안드로이드]dex 파일 65535 크기 넘어가는 오류 잡아 보기

2023. 1. 21. 19:40모바일프로그래밍/안드로이드

728x90
Unable to execute dex: method ID not in [0, 0xffff]: 65536)

 

위의 오류 메시지를 보고 있는 사람이라면, 분명히 이클립스 신봉자에다가 너무 큰 프로젝트를 맡고 있거나 아니면 앱을 만들기 위해서 오픈소스 라이브러리를 너무 많이 쓴 사람일 것이라고 확신한다. 이 문제에 대한 원인은 이미 나와 있다고 해도 틀린 말은 아니다.

 

[안드로이드]dex 파일 65535 크기 넘어가는 오류 잡아 보기

Unable to execute dex: method ID not in [0, 0xffff]: 65536) 위의 오류 메시지를 보고 있는 사람이라...

blog.naver.com

바로

'메소드 개수가 dex 파일을 만들기 위한 인덱스 개수인 65535개를 넘어섯으니, 메소드 개수를 줄이시오' 이다.

 

그러면 어떻게 이 문제를 해결 할 것인가를 두고 한번 얘기 해 보기로 한다.

아래 문서는 그래서 안드로이드 팀에서 이 문제를 어떤 식으로 해결 하였는 지를 말해주는 문서이다.

샘플코드도 제공 하고 있으니, 해당 내용을 가지고 한번 해보아도 될 듯하다.

사실, 위와 같이 만들어 보려고도 하지 않은 이유는 예전에 블로그에서 언급했던 동적 클래스 로딩에 대해서 완성해봐야 할 시점이 아닌가 하는 생각이 불현듯 들어서 예전 문서1에 내용을 다듬어서 다음과 같은 것들을 해보고 싶어서이다.

 

  1. 우선 클래스로더를 만들어 보고,
  2. 이 클래스로더를 사용하는 라이브러리를 만들고
  3. 래퍼 클래스를 만들어서 실제적으로 구동하는지 여부를 확인하기 위해서이다.

 

그럼 시작해 보자.

 

1. 라이브러리 만들기

결국 예전에 첫 안드로이드 라는 이름을 접했을때로 만들었던 문서2 말한 클래스 로딩 개념을 갖다 붙혀서 사용 해보도록 한다.

 

예전 문서에서도 언급 했듯이 제티의 AndroidClassLoader를 커스터마이징 했음을 밝힌다.

우선 다음과 같이 라이브러리를 만들 프로젝트를 하나 생성하자

 

 

내가 원하는 라이브러리는 jsoup-1.8.3과 커스터마이징 된 일련의 라이브러리 소스이며, 이 소스를 src 디렉토리에 복사해 넣도록 한다.

 

2. 라이브러리 만들기

이클립스에서 만들어진 일반 자바 라이브러리를 현재 달빅머신이 이해 할 수 있는 라이브러리 형태로 만들어 보자. 저번에 만들어 논 문서를 참고 해서 커맨드 창으로 해당 workspace 디렉토리에 다음을 수행 해보자.

dx --dex --output=".\ext-libs.jar" --positions=lines ".\bin"
 

dx.bat 파일은 내 경우 다음 디렉토리에 존재한다.

~\android-sdk\build-tools 하위에 여러 버전이 있는 데 가장 상위버전을 사용하도록 해보자.

 

3. 인터페이스 설계하기 

라이브러리를 만들었으니, 현재 앱에서 참조하고 있는 라이브러리를 빼보자. 당연히 라이브러리를 빼버리면, 에러가 나는 것은 당연하겠지…

 

그러면 고민에 한번 정도는 잠겨야 한다. 어떻게 해야 할 것인가?

지금 당장 생각 나는 것은 두가지가 있을 수 있을 것이다.

 

  • 완전하게 일반적인 클래스를 만들어 주는 것
  • 인터페이스를 만들어서 따로 구현 해 주는 것 

 

완전하게 일반적인 클래스를 만들어 주는 것은 정말 뭐랄까 힘든 일이다. 일일이 메서드 레벨까지 만들어서 완벽하게 넣어 주어야 하는 것이니까. 자바의 리플랙션 API를 사용해서 넣어야 하는 부담감도 있다. 

하지만 프레임웍을 만드는 작업을 한번 해 볼 수 있다는 잇점도 있겠지...

리플랙션 API에 대한 예를 보자면 다음 사이트 참고 해보자. 한국 사람은 아니고 중국계 미국 사람인 거 같은 데... 멘토 같은 사람이다.

 
 예제만 한 번 훑고 지나 가는 걸로..
//동적으로 AppTest 클래스를 런타임 시에 호출한다.
Class cls = Class.forName("com.mkyong.reflection.AppTest");
Object obj = cls.newInstance();

// printIt 메서드를 호출한다.
Method method = cls.getDeclaredMethod("printIt", noparams);
method.invoke(obj, null);

// printItString 메서드를 호출하고, 문자열 타입 param을 파라메터로 세팅한다
method = cls.getDeclaredMethod("printItString", paramString);
method.invoke(obj, new String("mkyong"));

// printItInt 메서드를 호출하고, int 타입param을 파라메터로 세팅한다
method = cls.getDeclaredMethod("printItInt", paramInt);
method.invoke(obj, 123);

// setCounter 메서드를 호출하고, int 타입param을 파라메터로 세팅한다
method = cls.getDeclaredMethod("setCounter", paramInt);
method.invoke(obj, 999);

//printCounter 메서드 호출
method = cls.getDeclaredMethod("printCounter", noparams);
method.invoke(obj, null);
 

위의 내용을 해가기에는 제약이 너무 많기도 하고 빠르게 개발하기에는 익숙하지 않을 수 있으니, 두 번째 인터페이스를 선언하고 구현하는 방법을 택해 보자.

내가 생각 했을 때, 일반적으로 두 번째 방법 문제를 해결하기 위한 전제는 내가 현재 코드를 완벽히 이해하고 있어야 한다는 것이다.

인터페이스로 일반적으로 만들어 주는 방법은 - 여기에서 ‘일반적’이라고는 용어는 클래스를 로딩하고 해당 클래스를 명시적으로 캐스팅 해주지 않고 인터페이스만으로 사용하기 위해서 라고 정의하면 될 듯하다

–정확히 다른 클래스가 원하는 결과물이 자바에서 말하는 일반적인 원시 타입 등이 아니면 적어도 String 타입이어야 한다는 것이다. 

예를 들어 보자면, 내가 InterfaceA 라는 클래스를 선언 할 때,

interface interfaceA
{
	public XXclass getXXClass()
	{
		return new XXClass();
	}
}
 

라고 한다면 이 XXclass 라는 것은 interfaceA에서 알아야 한다는 것이다.

이렇게 하지 않으려면 원시 타입으로 반환 할 수 있거나, 아니면 새로운 wrapper 인터페이스를 만들어야 한다는 결론이다.

본론으로 들어가서 나는 다음과 같이 ini 파일을 조작하기 위해서

 

  • IniFile 클래스를 선언 할 것이고,
private static IniFile mINI;
 
  • 이 클래스를 인스턴스화 할 것이며,
mINI = new IniFile();
 
  • 파일에서 읽어서 IniFile 객체에 저장하고,
mINI.read(iniReader);
 
  • 저장 한 값에 대해서 다음과 같이 찾기를 원한다.
mINI.getProperty(section, _key);
 

그리고 또 다른 기능으로서 Base64 인코딩/디코딩을 하기 원하며, 

String strB64 = new String(Base64.encodeBase64(contents));
Base64.decodeBase64(contents)
 

마지막으로 문자열에서 html 태그를 제거 하고, 또 이스케이프 문자가 있다면

이 문자 마저도 제거 했으면 좋겠다 라는 것이 이 인터페이스의 요구 사항이 된다.

StringEscapeUtils.unescapeHtml3(urlval);
Document doc3 = Jsoup.parse(StringEscapeUtils.unescapeHtml3(urlval));
doc3.outputSettings().escapeMode(Entities.EscapeMode.base); // default
doc3.outputSettings().charset("UTF-8");
 

대충 굵은 글씨 들이 내가 원하는 기능들을 만들어 내는 오픈 소스 들이다.

물론 대 전제인 메소드 개수를 낮추기 위해서 쉬엄쉬엄 이 오픈 소스에서 내가 원하는 기능만 뽑아 내는 것이다...늘 상 해 보았지만... 그게 만만하지만은 않다.

 

4. 인터페이스 만들기

위의 내용을 바탕으로 해서 다음과 같이 인터페이스 메서드를 만들도록 한다.

public String getProperty(String section, String key);
public String toUnescapeHtml(String str);
public String encodeBase64Str(String str);
 

최종 내용은 다음과 같이 되었다. 컴파일 오류 해결을 위해서 위의 메서드들을 주석 처리하고, 다음의 인터페이스로 대체한다. 이로써 인터페이스 클래스를 설계하고 만들어 보았다.

public interface LibInterface {
   String getProperty(String section, String key);
   String toUnescapeHtml(String str);
   String encodeBase64Str(String str);
}
 

5.  라이브러리 구현하기

그럼 위에서 나온 내용을 모두 몰아서 인터페이스 구현 클래스 인 LibInterfaceImpl 클래스를 만들어 보면 될 듯 하다. 우선 테스트를 위해서 구현을 본 앱에서 구현 하기로 한다.

 

한번 생성한 다음 이 클래스의 인스턴스 하나만을 사용하도록 일반적으로 싱글턴 클래스화 하기로 하자.

물론 클래스가 여러 번 로딩되는 상황에 대해서는 무시하기로 한다.

이름은 LibInterfaceImpl로 정하였고 소스 코드는 다음과 같다. 소스를 설명하는 자리는 아니므로 결과적으로 말하면 여러군데 흩어져 있던 라이브러리 사용 부분을 한데 합쳐서 인터페이스를 열어서 같이 쓰는 것이 결론이며 위에서 만든 ExtLibrary 안에 인터페이스 클래스와 함께 합쳐져서 들어가는 것으로 마무리 한다.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
//기본적으로 제공해 주는 API 가 아닌
// 내가 만들려는 라이브러리에 들어가는 클래스들에 대한 import
import net.sf.antcontrib.inifile.IniFile;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.net.util.Base64;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Entities;
import api.LibInterface;

public class LibInterfaceImpl implements LibInterface {
	private static LibInterfaceImpl _selfInstance;
	private static final String INI_LAYER_FILE_NAME = "lyr.setting.ini";
	private static final String INI_CONFIG_FILE_NAME = "XXXX.setting.ini";

	public static LibInterfaceImpl getSQLiteHelper() {
		if (_selfInstance == null) {
			synchronized (LibInterfaceImpl.class) {
				if (_selfInstance == null) {
					_selfInstance = new LibInterfaceImpl();
				}
			}
		}

		return _selfInstance;

	}

	private IniFile mLayerINI,mConfigINI;

	private LibInterfaceImpl()
	{
		mLayerINI = new IniFile();
		mConfigINI = new IniFile();
		configureEnvFromURL(INI_LAYER_FILE_NAME, mLayerINI);
		configureEnvFromURL(INI_CONFIG_FILE_NAME, mConfigINI);
	}

	private void configureEnvFromURL(final String inifile, final IniFile INI)
	{
		Reader iniReader = null;
		URL url = getClass().getResource(inifile);
		
		try
		{
			iniReader = new BufferedReader(new InputStreamReader(url.openStream()));
			INI.read(iniReader);
		}
		catch(IOException ie)
		{
			return;
		}
		finally
		{
			try{ iniReader.close(); }catch(IOException ie){}
			iniReader = null;
		}
		return;
	}

	@Override
	public String getProperty(final INI_TYPE iniType, final String section, final String key)
	{
		if(iniType == INI_TYPE.INI_CONFIG)
		{
			return mConfigINI.getProperty(section, key);
		}
		else if(iniType == INI_TYPE.INI_LAYER)
		{
			return mLayerINI.getProperty(section, key);
		}
		return null;
	}

	@Override
	public String toUnescapeHtml(final String str)
	{
		Document doc3 = Jsoup.parse(StringEscapeUtils.unescapeHtml3(str));
		doc3.outputSettings().escapeMode(Entities.EscapeMode.base); // default
		doc3.outputSettings().charset("UTF-8");
		return doc3.body().text();
	}

	@Override
	public String encodeBase64Str(final byte[] contents)
	{
		return new String(Base64.encodeBase64(contents));
	}

	@Override
	public void setProperty
		(final INI_TYPE iniType, final String section, final String key, final String value) 
	{
		if(iniType == INI_TYPE.INI_CONFIG)
		{
			mConfigINI.setProperty(section, key, value);
		}
		else if(iniType == INI_TYPE.INI_LAYER)
		{
			mLayerINI.setProperty(section, key, value);
		}
	}

	@Override
	public byte[] decodeBase64Str(byte[] contents) {
		return Base64.decodeBase64(contents);
	}
}
 

위에서 만들어진 ext-libs.jar 파일을 assets 폴더에 다음과 같은 폴더를 생성하고 복사해 놓는다.

6. Dex 파일 배치하기

위에서 만들어진 ext-libs.jar 파일을 assets 폴더에 다음과 같은 폴더를 생성하고 복사해 놓는다. 여러 다른 예제들 처럼 이번에 예제도 assets 폴더에 찾는 것으로 한다.

7. 클래스 로더 만들기

전체 소스 코드는 다음과 같다. 컴파일 되지 않을 것이지만 큰틀에서는 변함없는 데 우선 저장할 경로를 선택한다. (전체 소스 코드가 컴파일 되지 않는 부분은 몇개 되지 않을 것이다)

storagePath = XXXX.getStoragePath();
 

경로를 찾는 부분은 구글링 해보면 많이 있는 데, 자신에 맞는 경로를 입력하도록 한다. AssetManager를 선언하고,

mAssetManager = XXXX.getAssets();
 

asset 경로에 넣었기 때문에 이를 AssetManager로 찾아야 한다.

addClassPaths () 메서드에서 보면

  • AssetManager의 특정 디렉토리 여기서는 ext-libs 디렉토리 내에 있는 파일들의 리스트를 받아다가 안드로이드에 넣을 수 있는 파일이라면
  • “:” 를 붙여서 절대 경로를 이어 붙이고 나중에
  • DexClassLoader 클래스에서 이 경로를 읽어 들일 때 사용하도록 하는 것

이 주요 메커니즘이며, 전체 소스코드는 제티 소스에서 얻어다 커스터마이징 하였다.

import java.io.BufferedInputStream;
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.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;

import android.content.Context;
import android.content.res.AssetManager;

import com.tobee.FileManager;
import com.tobee.util.LogUtil;

import dalvik.system.DexClassLoader;
import dalvik.system.DexFile;

public class XXXXClassLoader extends ClassLoader
{
	private static final String TAG = XXXXClassLoader.class.getSimpleName();
	private static XXXXClassLoader _selfInstance;

	public static XXXXClassLoader getXXXXLibClassLoader() {
		if (_selfInstance == null) {
				synchronized (XXXXClassLoader.class) {
				// double checked locking - because second check of Singleton
				// instance with lock
				if (_selfInstance == null) {
					_selfInstance = new XXXXClassLoader();
				}
			}
		}

		return _selfInstance;

	}

	private ClassLoader _parent;
	private ClassLoader _delegate;
	private AssetManager mAssetManager;
	private static final String LIBPATH = "ext-libs";
	private static final int BUF_SIZE = 8 * 1024;
	private static final String DEFULT_DOWNLOAD_PATH="/Download/";
	private static final String TEMP_DOWNLOAD_PATH="/Mobile/temp/";
	private String path;
	private String storagePath;
	private List<String> libList;
	private List<String> classList;

	private XXXXClassLoader()
	{
		LogUtil.logD(TAG, ">>>>>>>>>>XXXXClassLoader");
		mAssetManager = Application.getAssets();
		storagePath = FileManager.getInstance().getStoragePath(false);
		libList = new ArrayList<String>();
		classList = new ArrayList<String>();

		try {
			init();
		} catch (IOException e) {
			LogUtil.printStackTrace(TAG, e);
		}catch (Exception e) {
			LogUtil.printStackTrace(TAG, e);
		}
	}

	public void addClassPaths() throws IOException
	{

		String[] fileList = mAssetManager.list(LIBPATH);
		String libraryPath = storagePath + TEMP_DOWNLOAD_PATH;
		//String libraryPath = Environment.getExternalStorageDirectory().toString() + TEMP_DOWNLOAD_PATH;
		LogUtil.logD(TAG, ">>fileList.length[%d]", fileList.length);
		String localpath = null;
		for(String file:fileList)
		{
			if ( isAndroidArchive( file ) )
			{
				if(isLibAlreayExists(file)) continue;
				
				libList.add(file);
				localpath = libraryPath + file;
				LogUtil.logD(TAG, ">>file[%s]path[%s]", file, localpath);
				addClassPath( file, localpath);
			}
		}
	}

	private boolean isLibAlreayExists(final String filename)
	{
		boolean isExists = false;
		String name = null;

		for(Iterator<String> iter = libList.iterator(); iter.hasNext(); )
		{
			name = iter.next();
			
			if(filename.equals(name))
			{
				isExists = true;
				break;
			}
		}
		return isExists;

	}

	/**
	* Accept a pre-made classpath.
	* NOTE: the path elements must be separated by ":" chars, not ";"
	* @see org.eclipse.jetty.webapp.WebAppClassLoader#addClassPath(java.lang.String)
	*/
	public void addClassPath( String fileName, String classPath ) throws IOException
	{

		BufferedInputStream bis = null;
		OutputStream writer = null;
		if ( classPath == null )
		{
		return;
		}

		File f = new File(classPath);

		if(f.canRead() && f.canWrite())
		{
			LogUtil.logD(TAG, "READ_WRITE_AVAILEABLE!!!!!!!!!");
		}

		//File dexInternalStoragePath = new File(AlopexApplication.sCurrentScreen.getDir(classPath, Context.MODE_PRIVATE),
		// LIBPATH+"/"+fileName);

		//File f = File.createTempFile("opt", "dex",
		// new File(AlopexApplication.sCurrentScreen.getCacheDir().getAbsolutePath()+"/"+fileName));

		try {
			bis = new BufferedInputStream(mAssetManager.open(LIBPATH+"/"+fileName));
			//bis = new BufferedInputStream(mAssetManager.open(classPath));
			writer = new BufferedOutputStream(
			new FileOutputStream(f));
			byte[] buf = new byte[BUF_SIZE];

			int len;
			while((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
			writer.write(buf, 0, len);
			}

			writer.flush();
			writer.close();
			bis.close();
		}
		catch (IOException ie)
		{
			LogUtil.printStackTrace(TAG, ie);
		}

		if(f.exists())
		{

			if(path == null)
				path = f.getAbsolutePath();
			else
			{
				if ( !"".equals( path ) && !path.endsWith( ":" ) )
				{
					path += ":";
				}

				path += f.getAbsolutePath();
			}
		}
		//Log.debug( "Path = " + path );
	}

	protected boolean isAndroidArchive( String filename )
	{
		int dot = filename.lastIndexOf( '.' );
		if ( dot == -1 )
		{
			return false;
		}

		String extension = filename.substring( dot );
		return ".zip".equals( extension ) || ".apk".equals( extension ) || ".jar".equals( extension )|| ".dex".equals( extension );
	}
    
    private void init() throws IOException
	{

		LogUtil.logD(TAG, ">>init");
		//String storagePath = FileManager.getInstance().getStoragePath(false);
		//String tmpFilePath = storagePath + TEMP_DOWNLOAD_PATH;
		//File tmpFile = new File(tmpFilePath);
		//LogUtil.logD(TAG, ">>Temp path[%s]", tmpFilePath);
		///System.setProperty("dexmaker.dexcache", storagePath);

		final File optimizedDexOutputPath = AlopexApplication.sCurrentScreen.getDir("outdex", 0);
		//File f = new File(Environment.getExternalStorageDirectory().toString() + dexFile);
		_parent = this.getClass().getClassLoader();

		if (_parent == null)
			_parent = ClassLoader.getSystemClassLoader();

		//path = tmpFilePath;
		addClassPaths();

		LogUtil.logD(TAG, "Path [%s]", path);

		if (path==null || "".equals(path.trim()))
			_delegate = new DexClassLoader("", optimizedDexOutputPath.getAbsolutePath(),null,_parent);
		else
			_delegate = new DexClassLoader(path, optimizedDexOutputPath.getAbsolutePath(), null, _parent);

		//_delegate = new DexClassLoader(path, File.createTempFile("opt", "dex",
		// AlopexApplication.sCurrentScreen.getCacheDir()).getAbsolutePath(), null, _parent);
		setAllClasses(path);
		LogUtil.logD(TAG, ">> exit init");
	}


	public synchronized java.io.InputStream getResourceStream(String name)
	{
		java.io.InputStream in = null;

		if(_delegate != null)
		{
			in = _delegate.getResourceAsStream(name);
		}
			return in;

	}

	public List<String> getClassList()
	{
		return classList;
	}

	private void setAllClasses(String dpath)
	{
		//String path = "/path/to/your/library.jar"

		try {
			DexFile dx = DexFile.loadDex(dpath, File.createTempFile("opt", "dex",
			AlopexApplication.sCurrentScreen.getCacheDir()).getPath(), 0);

			// Print all classes in the DexFile
			for(Enumeration<String> classNames = dx.entries(); classNames.hasMoreElements();) {
				String className = classNames.nextElement();
				classList.add(className);
				LogUtil.logD(TAG, "class: [%s]", className);

			}

		} catch (IOException e) {
			LogUtil.printStackTrace(TAG, e);
		}
	}

	public synchronized java.net.URL getResource(String name)
	{

		URL url= null;
		boolean tried_parent= false;

		if (url == null)
		{
			url= this.findResource(name);

			if (url == null && name.startsWith("/"))
			{
				url= this.findResource(name.substring(1));
			}
		}

		if (url == null && !tried_parent)
		{
			if (_parent!=null)
				url= _parent.getResource(name);
		}

		LogUtil.logD(TAG, "getResource("+name+")=" + url);

		return url;
	}


	public synchronized Class<?> loadClass(String name) throws ClassNotFoundException
	{
		return loadClass(name, false);
	}

	protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
	{

		LogUtil.logD(TAG, "Enter loadClass[%s]", name);

		Class<?> c= findLoadedClass(name);
		ClassNotFoundException ex= null;
		boolean tried_parent= false;

		//LogUtil.logD(TAG, "loadClass from parent..", name);
		if (c == null && _parent!=null )
		{
			tried_parent= true;
			
			try
			{
				c= _parent.loadClass(name);
			}
			catch (ClassNotFoundException e)
			{
				//LogUtil.logD(TAG, "1..loadClass... failed[%s]", e.getMessage());
				ex= e;
			}

		}

		LogUtil.logD(TAG, "loadClass from me..");

		c = null;

		if (c == null)
		{
			try
			{
				LogUtil.logD(TAG, "loadClass from me..[%s]", _delegate.toString());
				//c = _delegate.loadClass("net.sf.antcontrib.inifile.IniFile");
				c= (Class<?>)_delegate.loadClass(name);
				//XXXXLibInterface ee = (XXXXLibInterface) c.newInstance();
				LogUtil.logD(TAG, "2..finally....loadClass... [%s]", c.toString());
			}
			catch (ClassNotFoundException e)
			{
				LogUtil.logD(TAG, "2..loadClass... failed[%s]", e.toString());
				ex= e;
			}
			catch (Exception e)
			{
				LogUtil.logD(TAG, "2..loadClass... failed[%s]", e.toString());
			}
			catch (Error e)
			{
				LogUtil.logD(TAG, "2..loadClass... failed[%s]", e.toString());
			}

		}

		LogUtil.logD(TAG, "loadClass from me..");

		//if (c == null && _parent!=null && !tried_parent )
		// c= _parent.loadClass(name);

		if (c == null)
		{
			LogUtil.logD(TAG, "finally....loadClass... failed");
			throw ex;
		}

		if (resolve) resolveClass(c);

		LogUtil.logD(TAG, "loaded " + c+ " from " + c.getClassLoader());

		return c;
	}

	@Override
	public String toString()
	{
		return "(AndroidClassLoader, delegate=" + _delegate + ")";
	}
}
 

8. 실행하고 에러 잡기

이렇게 해서 마무리가 되면 앱을 직접 설치 해보기로 하자 앱을 설치하고 다음과 같은 오류 메시지가 확인 되었다.

임시 디렉토리에 대한 권한 오류 인가? 무슨 문제인지 구글링 해보면…

문제가 된 init 메서드를 다음과 같이 바꾸어 주었다.

private void init() throws IOException
{
	final File optimizedDexOutputPath = getDir("outdex", 0);
	_parent = this.getClass().getClassLoader();

	if (_parent == null)
		_parent = ClassLoader.getSystemClassLoader();

	addClassPaths();

	LogUtil.logD(TAG, "Path [%s]", path);

	if (path==null || "".equals(path.trim()))
		_delegate = new DexClassLoader("",
			optimizedDexOutputPath.getAbsolutePath(), null,_parent);
	else
		_delegate = new DexClassLoader(path,
			optimizedDexOutputPath.getAbsolutePath(), null, _parent);
}
 

9. 여기서 끝인가요?

만약 다음과 같은 에러가 발생 하였다면?

java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
 

거의 하루를 삽질을 하고 위에 문서를 검토 해보니 나오는 문제는 바로 인터페이스의 중복 문제였다.

인터페이스로 만든 클래스를 현재 라이브러리로 사용하는 패키지와 앱에서 사용하는 패키지에 중복으로 들어가 있어서 이런 현상이 발생을 하였다.

 

만약 이 상황에서 구현된 클래스를 부르면 자연히 implements 키워드에 있는 인터페이스를 부모 클래스로더에서 먼저 찾게 되지만, 이후에 커스텀 클래스로더가 라이브러리를 메모리에 올려 놓는 순간 자신의 클래스 경로에 있는 인터페이스 클래스를 호출하려 할 것이나 이미 부모 클래스로더에서 이 인터페이스를 로딩 하였으므로, 위의 오류 상황이 발생 하였다.

 

이런 경우 Error 클래스로 분류가 된 이유는 사용자가 이 문제를 런타임 시에 찾아내더라도 해 줄 것이 없다는 의미 일것이다.

 

따라서 dex 파일을 만들기 전에 클래스 파일 중에서 인터페이스 클래스를 삭제하고 dex 파일을 묶어주어야 한다는 것이다.

 

절차를 말하자면

  1. rd /S /Q bin
  2. 이클립스에서 project --> clean 해당 프로젝트 선택
  3. del /Q .\ExtLibrary\bin\com\test \api\* : 인터페이스 클래스 삭제
  4. dx --dex --output=".\ext-libs.jar" ".\bin" : dex 파일을 비롯한 리소스 파일 묶기
  5. move ext-libs.jar .\workspace\myproject\assets\ext-libs 해당 assets 디렉토리로 옮기기
  6. ant clean debug install 디버그모드로 인스톨
  7. 혹은 ant clean release install 릴리즈 모드로 인스톨
  8. ant run 앱 실행
  9. 클래스 로더 사용하기

 

그러면, 이렇게 만들어진 클래스로더를 어떻게 사용 할 것인가에 대한 문제만 남은 듯 하다. 이름을 XXXResources 라고 붙혔다

전체 소스를 보면

import java.util.Iterator;
import java.util.List;

public class XXXXResources {
	private final static String TAG = XXXXResources.class.getSimpleName();
	private static volatile int refCount = 0;
	private static XXXXLibInterface intfce;

	public static XXXXLibInterface loadXXXXInterface()
	{
		Class<?> libProviderClazz = null;
		
		try 
		{
			LogUtil.logD(TAG, "Load the library....");// Load the library.
			XXXXClassLoader tclsLoader = XXXXClassLoader.getXXXXLibClassLoader();
			//libProviderClazz = Class.forName("com.tobee.api.impl.XXXXLibInterfaceImpl");
			//Plugin plugin = (Plugin) classObject.getMethod(
			// "getInstance", nullParameter).invoke(nullObject, args);
			//tclsLoader.loadClass("com.tobee.api.XXXXLibInterface");
			libProviderClazz = tclsLoader.loadClass("com.api.impl.XXXXLibInterfaceImpl");
			
			// Cast the return object to the library interface so that the
			// caller can directly invoke methods in the interface.
			// Alternatively, the caller can invoke methods through reflection,
			// which is more verbose.
			LogUtil.logD(TAG,
			"Load the library....okey[%s]", libProviderClazz.toString());
			
			intfce=(XXXXLibInterface) libProviderClazz.newInstance();
			
			//byte[] contents = "this is a contents".getBytes();
			//String s = intfce.encodeBase64Str(contents);
			
			LogUtil.logD(TAG, "encoded string[%s]", s);
			refCount++;
			
		} catch (Exception e)
		{
			LogUtil.printStackTrace(TAG, e);
		}
		return intfce;
	}

}
 

 

10. 마치면서

마지막에 이 말을 함 써보고 싶어서 써봤다. 다 써 주더만…

여하튼 결론을 말하자면, 클래스로딩을 안드로이드에서 사용하기 위해서는 무지하게 힘든 과정을 거쳐야만 한다는 것이다.

 

먼저 자바 라이브러리 프로젝트를 하나 만들었다 (자바 1.7)

그리고 컴파일 된 클래스 파일들을 안드로이드가 인식 할 수 있도록 dex.dat 파일로 dex 파일을 만들어 주었다. - 여기서 주의 할 점은 jar나 zip 파일로 만들어야 한다는 것이다.

그리고 라이브러리 만들고, 커스텀 클래스로더, 마지막으로 클래스로더를 사용하여 클래스들은 읽어들이는 클래스 까지 만들어 본 걸로 친다.

 

안드로이드 스튜디오를 사용해 보는 것도 좋은 방법인 것 같으나, gradle 인가 뭐시긴가가 걸리기도 하고 체질상 IDE를 좋아하지도 않는 바, ant 환경에 최적화 하여 사용하여도 무방할 듯하다. 위에서 언급한 내용을 담은 샘플 사이트 주고는 아래와 같다.

위 사이트에서 샘플을 테스트하고 프로젝트에 적용하는 방법을 한번 긁적여 봐야 겠다.

이상.


 

728x90