در اعماق گریدل (gradle)

به عنوان یه برنامه نویس اندروید قطعا از گریدل استفاده کردید و بهش ارادت خاصی دارید . تو این مقاله هدفمون اینه که یه قدم فراتر بریم و ببینیم که گریدل اصلا چیه ، چه طور کار میکنه ، از چه مکانیسم هایی برای کش استفاده می کنه ، آیا میتونیم سرعت گریدل رو بالا تر ببریم یا به طور مادرزادی کنده و در نهایت اینکه چه طور می تونیم برای گریدل تسک بنویسیم و کلی چیز دیگه ...اما قبل از اینکه شروع کنیم به یه سوال مهم جواب میدیم :به عنوان توسعه دهنده اندروید اصلا چرا باید با سازوکار گریدل آشنا باشیم ؟ درسته که گریدل خودش همه ی کار هارو انجام میده و ما معمولا در حد اضافه کردن یه سری پلاگین و دیپندنسی باهاش کار می کنیم ، اما همون طور که میدونید بخش قابل توجهی از زمان ما هم برای فیکس یا بیلد گریدل از دست میره .آشنایی با نحوه کار گریدل شاید بتونه به ما کمک کنه تا زمان هایی که با ارور های گریدل درگیر هستیم کاهش پیدا کنه و زودتر بتونیم علت خطا رو پیدا کنیم .یا اینکه بتونیم با اقداماتی سرعت بیلد گریدلمون رو افزایش بدیم .یا حتی شاید در شرایطی قرار بگیریم که لازم باشه به گریدل تسک های کاستوم خودمون رو اضافه بکنیم. (مثلا در مواردی که در حال توسعه ی یک لایبرری یا تعریف یک قرارداد برای پروژه ی خود هستیم )اگر شما هم مثل من از اون دسته آدم هایی هستید که دوست دارید صرفا یه استفاده کننده نباشید و از بکگراند همه چیز سر در بیارید تا انتهای این مقاله همراه من باشید .گریدل چیست ؟طبق گفته ی داکیومنتش :گریدل یه build automation tool اپن سورسه که طوری طراحی شده تا با انواع مختلفی از نرم افزار ها سازگار باشه . Gradle در مورد اونچه که می خواهیم بسازیم یا چگونگی ساختنش، فرضیات کمی داره. و همین باعث می شه که Gradle انعطاف پذیری بالایی داشته باشه.اما حالا build automation tool ها چه ابزار هایی هستن؟ابزار هایی هستن که فرایند تولید یک نرم افزار (به طور مثال شامل کامپایل کد ها ، ساختن پکیج ها و اجرای تست ها) را به صورت اتوماسیون شده انجام می دهند.از برخی از این ابزار ها می توان به Make, Rake, CMake, MSBuild, Ant, Maven , ... اشاره کرد.مفاهیم پایه (basic concepts) :در این جا با برخی از اصطلاحات و موارد پایه ای گریدل آشنا میشیم :پروژه : یک پروژه اصطلاحا چیز هایی است که گریدل آن هارا بیلد می کند . هر پروژه شامل یک فایل build script است که معمولا در دایرکتوری ریشه ی پروژه نگه داری می شود و معمولا به نام های build.gradle یا build.gradle.kts قابل مشاهده است .این فایل شامل تعاریف تسک ها ، دیپندنسی ها ، پلاگین ها و سایر کانفیگ های پروژه است .تسک : تسک ها یا وظایف گریدل شامل لاجیک کارهایی است که گریدل باید انجام دهد تا بتواند بیلد بگیرد ، تست هارا اجرا کند و یا فرایند هایی که بتواند نرم افزار را دیپلوی کند.در گریدل تسک های بسیاری به صورت پیشفرض وجود دارد اما در برخی از موارد شرایطی پیش می آید که بخواهیم تسکی را اضافه کنیم یا پیاده سازی تسک های موجود را تغییر دهیم و یا آن ها را بسط دهیم .در گریدل هر تسک شامل 3 مورد است * :1. اکشن (Actions) : اکشن قسمتی است که کار و عملی را انجام می دهد -> به طور مثال فرایند کامپایل کردن یک کد 2. ورودی (Inputs) : ورودی ها شامل فایل ها ، مقادیر و یا دایرکتوری هایی است که اکشن ها از آن ها استفاده می کنند و یا روی آن ها عملی انجام می دهند -> به طور مثال کد های نوشته شده توسط برنامه نویس که قرار است کامپایل شوند.3. خروجی (Outputs) : فایل ها و یا دایرکتوری هایی که اکشن ها آن ها را ایجاد کرده اند و یا تغییری روی آن اعمال کرده اند -> به طور مثال کد کامپایل شده که توسط اکشن تولید شده است .از این مفاهیم در آینده برای توضیح مکانیسم های کش گریدل و ایجاد تسک ها استفاده می کنیم ، برای درک بهتر آن می توانید یک فانکشن در برنامه نویسی را تصور کنید که ورودی هایی را دریافت کرده و روی آن ها تغییراتی اعمال می کند و مقدار تغییر یافته را به خروجی ارسال می کند . (البته که مانند یک تابع وجود ورودی و خروجی همیشه الزامی نیست .)فایل gradle-wrapper : یک اسکریپت اجرایی است که یک نسخه ی خاص از گریدل در آن تعریف شده است و هنگام اجرا بررسی میکند که ایا گریدل بر روی سیستم جاری نصب شده است یا خیر . در صورت نصب نبودن آن را دانلود کرده و نصب می کند . به این ترتیب از انجام دستی آن توسط دولوپر جلوگیری می کند .نکته : در آینده خواهیم دید که برای اجرای دستورات گریدل از طریق کامند لاین به دو طریق می توانیم عمل کنیم :1) استفاده از کامند gradle به طور مثال (gradle build)2) استفاده از کامند gradlew به طور مثال (gradlew build)اما تفاوت این دو در چیست ؟ هر دو یک خروجی را ایجاد می کنند اما تفاوت آن ها در نحوه ی اجراست اگر با کامند gradle دستورات را اجرا کنید گردیل باید حتما روی سیستم شما نصب باشد و path آن به سیستم معرفی شده باشد ، اما اگر از کامند gradlew استفاده کنید اجرای دستور از کانال gradle-wrapper عبور می کند ، به این معنا که اگر گریدل روی سیستم شما نصب نباشد ابتدا فایل wrapper را اجرا کرده و آن را نصب و کانفیگ می کند ، سپس دستور شما را اجرا خواهد کرد (از fail شدن دستور به دلیل نبود gradle جلوگیری می کند.)پلاگین ها : قطعا تا کنون پیش آمده است که پلاگین هایی را به گریدل خود اضافه کنید مثلا در مواردی که پلاگین های dagger را به پروژه خود اضافه کردید .pluginsپلاگین ها به شما این امکان را می دهند تا بتوانید تسک ها و کانفیگ ها را فراتر از آن چه در گریدل (به صورت پیشفرض) تعریف شده است به پروژه ی خود اضافه کنید .به کمک پلاگین ها می توانید یک تسک یا کانفیگ را یکبار ایجاد کرده و در سورس های مختلفی از آن استفاده کنید . (درست مانند یک لایبرری که بعد از تعریف دیپندنسی آن ، کد هایش به پروژه ی شما اضافه می شود)مثلا هنگامی که پلاگین dagger را در پروژه تعریف می کنید ، این پلاگین تسک های مورد نیاز dagger را به گریدل شما اضافه می کند و به کمک این تسک ها می تواند کلاس های مورد نیاز را جنریت کند .فاز های بیلد (Build phases) : گریدل دارای lifecycle مشخصی است که شامل 3 فاز است * :1. فاز Initialization -> محیط بیلد را ایجاد می کند و کانفیگ های لازم را بر اساس فایل settings.gradle اعمال می کند . 2. فاز Configuration -> در این مرحله build.gradle خوانده و تفسیر می شود سپس گریدل گراف تسک ها را ایجاد می کند و آن را پیکر بندی می کند. گراف تسک ها *: این گراف مانند یک درخت برعکس است که ریشه در بالا ترین قسمت آن قرار دارد و مشخص می کند که کدام یک از تسک ها و با چه ترتیبی باید اجرا شوند .دو تسک A و B را در نظر بگیرید که خروجی تسک A ورودی تسک B است (B به A وابستگی دارد) در این صورت تسک A حتما باید قبل از B اجرا شود ، گریدل این ترتیب را در گراف تسک ها مشخص می سازد .3. فاز Execution -> در این فاز که همان فاز اجرایی است ، گریدل بر اساس گراف تسک ها ، آن ها را اجرا می کند .بیش تر زمان در گریدل صرف تسک های اول و دوم می شود ، ایجاد گراف تسک ها یک عمل پیچیده است و کوچکترین خطا در آن می تواند تمام گراف را بی اعتبار کند و مشکلات جدی ایجاد کند.بیلد (Build) : بیلد در گردیل شامل اجرای مجموعه ای از تسک ها است که توسط command line یا ide ها اجرا می شود . گریدل همواره سعی می کند تا وظایف اصلی خود را با اجرای کم ترین تسک های ممکن به پایان برساند گریدل دیمون (Gradle Daemon) : شاید متوجه شده باشید که در برخی از موارد با اینکه استفاده ی خاصی از اندروید استادیو ندارید (صرفا در حال کد زدن هستید) اما همچنان مصرف ریسورس ها توسط ide بالاستاما چرا ؟ در این جا به تعریف دیمون می پردازیم :دیمون ها فرایند های پردازشی طولانی مدتی هستند که همواره در بکگراند اجرا می شوند و به کمک آن ها می توان build time را کاهش داد .به بیان ساده تر گریدل برخی از وظایف خودش رو به جای اینکه در زمان بیلد انجام بده در زمان هایی که مشغول بیلد نیستیم انجام میده و باعث میشه تا زمان بیلد به طور قابل توجهی کاهش پیدا کنه .برخی از این وظایف شامل موارد زیر است :1. کش کردن اطلاعات و وضعیت پروژه که در حین بیلد تولید شده است .2. فعال نگه داشتن Jvm ، بنابراین در هر بیلد نیاز به انتظار برای فعال شدن jvm ندارید .3. بهینه سازی های مداوم زمان اجرا در jvm .4. بررسی فایل سیستم برای محاسبه دقیق آنچه نیاز دارد قبل از اجرای یک بیلد بازسازی شود.نکته : دیمون به صورت پیشفرض فعال است اما اگر در مواردی که با کمبود ریسورس ها به خصوص ram مواجه هستید (به طور مثال در سرور های CI) میتوانید آن را غیر فعال کنید (البته که انجام این کار زمان بیلد را افزایش می دهد و توصیه شده نیست)برای غیر فعال سازی آن به فایل gradle.properties مراجعه کنید و خط زیر را به آن اضافه کنید:org.gradle.daemon=falseکامند های گریدل (Gradle CLI) : برای اجرای تسک های اجرایی گریدل می توانید علاوه بر Ide از کامند های گریدل نیز استفاده کنید .ساختار کامند ها به شکل روبرو است :gradle/gradlew + taskNameبه طور مثال برای بیلد گرفتن از پروژه در اندروید استادیو دوبار بر روی CTRL کیبورد کلیک کنید و در کادر باز شده کامند بیلد (gradle assembleDebug) را وارد کنید و ENTER بزنید آنگاه تسک بیلد شروع به اجرا می کند. همچنین در کادر باز شده سایر کامند ها به همراه توضیحات آن ها موجود است .When you understand how to move from using Gradle via all the green buttons, to issuing actual CLI commands, you become the real Android developer ??مکانیسم های کش (cache) در گریدل :حالا بریم سراغ یکی از قسمت های مهم گریدل ? مکانیسم های کش کمک میکنن تا بتونیم زمان زیادی رو به وسیله ی استفاده ی مجدد از output های تولید شده در بیلد های قبلی save کنیم . قطعا متوجه این شده اید که اولین بیلد معمولا زمان زیادی طول میکشه اما در بیلد های بعدی این زمان به شدت کاهش پیدا میکنه (تسک های بدون تغییر اجرا نمی شوند و لیبل UP_TO_DATE می گیرد)، این به دلیل همان کش های صورت گرفته در گریدل است که اگر وجود نداشت قطعا تمام بیلد های شما به همان اندازه ی اولین بیلد پروژه طول می کشید مکانیسم های کش output های تولید شده از تسک ها را به صورت لوکال (روی سیستم کاربر) یا ریموت (روی یک سرور مرکزی) ذخیره میکند و به تسک های بعدی اجازه می دهد تا از این خروجی ها استفاده کنند البته به شرطی که تشخیص دهد input ها تغییری نداشته اند .در نوشتن تسک های کاستوم باید ورودی و خروجی های تسک را آنقدر واضح تعیین کنیم تا گریدل بتواند خروجی تسک ما را کش کند و همچنین با انجام برخی از اقدامات گریدل را متوجه می کنیم تا از اجرا های بی مورد تسک ما پرهیز کند.حالا با برخی از این مکانیسم های کش آشنا میشیم :M) incremental buildsقطعا همه ی ما نتایج بیلد های افزایشی را دیده ایم . محل قرار گیری نتایج این نوع از بیلد در پروژه ، همان دایرکتوری build است که کد های جنریت شده قرار می گیرند به طور مثال کد هایی که توسط Dagger یا Binding جنریت می شوند .نکته : به کمک دستور gradlew assembleDebug در پروژه های اندرویدی می توان نتایج این بیلد را ایجاد کرد و به کمک دستور gradlew clean می توان این نتایج را پاک کرد .اولین قدم برای سرعت بخشیدن به بیلد های ما، به حداکثر رساندن استفاده مجدد از آنچه Gradle در این دایرکتوری ها قرار می دهد است (به شرطی که محتوای فایل های جنریت شده تغییر نکرده باشد) . Gradle این استفاده مجدد را "incremental building" می نامد.A) build cacheکش قبلی (incremental) تنها روی پروژه ی جاری بر روی سیستم شما اعمال می شود اما در این نوع از بیلد شما میتوانید output هارا بین پروژه های مختلف روی یک سیستم و یا حتی روی یک ماشین دیگر (به طور مثال بر روی یک سرور مرکزی) تولید کنید و سایر دولوپر ها صرفا از این خروجی ها استفاده کنند.این یه امر منطقیه تصور کنید یک پروژه ی مثلا اندرویدی به نام A و یه پروژه ی دیگه به نام B روی لب تابتون دارید ، کش های حاصل از بیلد افزایشی A , B را نمی توانید برای هم دیگر استفاده کنید چون فایل های جنریت شده برای هر پروژه مجزا است ، اما موارد کش شده توسط build cache در تمام پروژه های شما (A , B , ...) قابل استفاده است .در این نوع از بیلد ها هم مشابه بیلد افزایشی در صورت یکسان بودن ورودی و به تناسب آن خروجی ، گریدل تسک را مجددا اجرا نمی کند و صرفا از خروجی بیلد قبلی استفاده میکند ، با این تفاوت که برخلاف بیلد افزایشی که خروجی را صرفا از دایرکتوری build فراخوانی می کرد و صرفا روی یک ماشین کار می کرد می تواند خروجی ها را از هر بیلدی در هر کجای ماشین شما یا ماشین های دیگر فراخوانی کند.* این نوع از کش به طور پیشفرض روی گریدل فعال نیست و برای فعال سازی آن باید لاین زیر را به gradle.properties اضافه کنید:org.gradle.caching=trueH) Android’s build cacheدو بیلد قبلی در هر گریدل پروژه ای موجود بود ، اما این نوع از بیلد صرفا مخصوص پروژه های اندرویدی است که توسط Android Gradle Plugin (AGP) تعدادی مکانیسم کش جدید به پروژه های اندرویدی اضافه می کند .بیلد کش (build cache) ای که در مورد قبلی معرفی شد برای پیچیدگی های یک پروژه ی اندرویدی کافی نبود به همین دلیل بود که android build cache معرفی شد.وظیفه ی این بیلد کش ذخیره ی outputs های خاصی است که توسط android plugin برای گریدل هنگام بیلد پروژه ی ما تولید می کند است . (مانند AARs)کش های اندروید همچنین روی سرورهای یکپارچه سازی پیوسته (CI) و هنگام اجرای چندین پروژه بر روی یک ماشین محلی در دسترس است.نکته: این کش ها در دایرکتوری users/home/.android/.build_cache/ ذخیره می شوند (البته محل های احتمالی دیگری برای ذخیره ی آن ها وجود دارد ) و می توان با اجرای دستور gradlew cleanBuildCache آن ها را پاک کرد.S) Memory cache with the Daemonدر هر یک از بیلد قبلی‌مان، ما یک کش دیگر داشتیم که به نفع ما کار می‌کرد که قبلا در رابطه با آن صحبت کرده ایم . این ذخیره سازی توسط Gradle Daemon ارائه شده است.مزیت آشکار دیمون این است که Gradle فقط یک بار برای چندین بیلد در حافظه بارگذاری می شود، نه یک بار به ازای هر بیلد.کد به تدریج در طول اجرا بهینه می شود، به این معنی که ساخت های بعدی می توانند به دلیل این فرآیند بهینه سازی سریعتر باشند.Daemon همچنین اجازه می دهد تا کارایی بیشتری در کش کردن حافظه در سراسر بیلدها داشته باشد. برای مثال، کلاس‌های مورد نیاز بیلد (مانند پلاگین‌ها، اسکریپت‌های ساخت - build scripts) را می‌توان در حافظه بین بیلد ها نگه داشت.A) Third-party dependency cachingحافظه نهان دیگری که Gradle ارائه می دهد، کش کردن دیپندنسی های شخص ثالث است. اکثر برنامه های اندروید حداقل به چند دیپندنسی شخص ثالث وابسته هستند. حتی اگر فقط از کدهای AndroidX libs استفاده می کنید، باز هم دیپندنسی هایی دارید که باید از مخازن مرکزی دانلود شوند. Gradle این وابستگی ها را در Dependency Cache با نام مناسب ذخیره می کند.درست مانند build cache، این کش در تمام پروژه ها در یک دستگاه موجود است. (به همین دلیل است که اگر دو پروژه اندرویدی با دیپندسی هایی که ورژن مشابه دارند روی یک سیستم ایجاد کنید ، دیپندنسی ها را یکبار دانلود می کند و در پروژه ی دوم از همان ها استفاده میکند)گریدل این کش ها را در GRADLE_HOME/.gradle/caches ذخیره می کند .بهینه سازی بیلد تایم در گریدل تاکنون از بخش های قبلی با سه عامل موثر در بهینه سازی بیلد تایم گریدل آشنا شدیم :1) بهینه سازی به کمک کش کردن خروجی های هر تسک 2) بهینه سازی به کمک کش کردن وابستگی ها و لایبرری ها 3) بهینه سازی به کمک اجرای daemonاکنون به عوامل دیگری که می توانند به افزایش سرعت بیلد کمک کنند 4) configuration cache (experimental)همانطور که در بخش فاز های گردیل صحبت کردیم بیش تر زمان گریدل صرف فاز های Initialization و Configuration می شود . تاکنون تمام مکانیسم های کش که در رابطه با آن ها صحبت کردیم در فاز Execution بودند . اما برای افزایش حداکثری سرعت بیلد احتیاج داریم تا مکانیسم هایی برای کش در فاز های اول و دوم پیاده سازی کنیم . اما بهینه سازی در این دو مرحله به هیچ وجه کار آسان و بی خطری نیست ، کوچکترین خطا در این دو مرحله می تواند باگ های فراوان و غیر قابل کشف ایجاد کند .اما اکنون تیم توسعه دهنده ی گریدل توانسته فیچری آزمایشی به جهت کش در مرحله ی Configuration ارایه دهد :کانفیگ کش مکانیسمی است که به کمک آن می توانیم گراف تسک های تولید شده توسط گریدل را که قبلا پیرامون آن صحبت کردیم کش کنیم . در حالت پیشفرض هر بار که بیلد می گیرید گریدل گراف تسک ها را مجددا تولید کرده و از آن استفاده می کند اما به کمک این قابلیت گریدل فقط زمانی گراف را مجددا ایجاد می کند که متوجه شود تغییری در آن رخ داده است.این قابلیت هم اکنون به صورت پیشفرض در گریدل فعال نیست و تیم گریدل همچنان در حال توسعه ی آن است (experimental) برای فعال سازی این نوع از کش خط زیر را به gradle.properties اضافه کنید:org.gradle.unsafe.configuration-cache=true5) parallel executionدر اکثر پروژه ها ، زیر پروژه (subproject) هایی وجود دارد (مانند معماری ماژولار در پروژه های اندرویدی) که در وضعیت اشتراک ریسورس ها یا وابستگی نیستند .یعنی ماژول ها از هم مستقل هستند و به یکدیگر دیپندنسی ندارند ، در چنین شرایطی گریدل می تواند تسک هارا به صورت موازی جلو ببرد بدون آنکه مشکلی در سایر قسمت ها ایجاد شود.به صورت پیشفرض گریدل فقط قابلیت اجرای یک تسک در آن واحد را دارد اما با افزودن خط زیر به gradle.properies گریدل از قابلیت پردازش موازی بهره مند می شود.org.gradle.parallel=trueتاثیر پردازش موازی بر زمان بیلد پروژه وابسته به معماری پروژه و میزان وابستگی های آن است.6) Re-enable the Gradle Daemonما در بخش های قبل به طور مفصل در رابطه با دیمون صحبت کردیم .دیمون به صورت پیشفرض روی گریدل فعال است.اما در برخی از شرایط بیلد ما ممکن است دیمون را غیر فعال کند، برای جلوگیری از این اتفاق می توانیم آن را override کنیم به این منظور در فایل gradle.properties لاین زیر را اضافه می کنیم :org.gradle.daemon=true7) Increase the heap sizeگریدل به صورت پیشفرض 512 مگابایت از فضای heap را برای بیلد ما اختصاص می دهد ، این مقدار در اکثر پروژه ها کافی است ، اما اگر بیلد ها سنگین هستند و به فضای بیش تری احتیاج دارند این مقدار را به کمک افزودن خط زیر به gradle.properties می توان افزایش دادorg.gradle.jvmargs=-Xmx2048M8) Optimize repository orderاین یک نکته ی جالب در گریدل است ?شما با تغییر ترتیب مخازن خود می توانید سرعت بیلد خودتون رو افزایش بدید. به کد زیر توجه کنید repositories { mavenLocal() mavenCentral() google() // other repositories go here }هنگامی که گریدل در حال پیدا کردن دیپندنسی ها است به صورت پیشفرض آن ها را در مخازن تعریف شده از بالا به پایین جستجو می کند.بنابراین اگر بدانید دیپندنسی های شما بیش تر در کدام مخازن واقع شده اند با تغییر اولیت آن می توانید سرعت گریدل رو بالا ببرید . پیشنهاد می شود که ترتیب مخازن را به صورت زیر حفظ کنید:مخازن محلی: مخازنی که در سیستم شما نصب شده‌اند و شامل وابستگی‌هایی هستند که شما خودتان تولید کرده‌اید.مخازن (jcenter): jcenter یک مخزن مرکزی برای وابستگی‌های مختلف است که به صورت پیشفرض در Gradle تنظیم شده است. این مخزن شامل بیش از ۲۰۰ هزار وابستگی است و برای پیدا کردن بیشتر وابستگی‌ها اولین مخزنی است که باید در ترتیب مخازن در Gradle تعریف شود.مخازن مربوط به گوگل (Google): این مخازن شامل وابستگی‌هایی هستند که توسط گوگل ارائه شده‌اند، از جمله کتابخانه‌های مربوط به پشتیبانی از خدمات گوگل، مانند Google Play Services و Firebase.مخازن مربوط به سایر توسعه دهندگان: مخازنی که توسط توسعه‌دهندگان مختلف برای ارائه وابستگی‌های خود استفاده می‌کنند، مانند مخازن برای وابستگی‌های مربوط به کتابخانه‌های متن‌باز.* البته نکته ای که مطرح است گوگل به تازگی اعلام کرده که بزودی jcenter را خاموش می کند ، دلیل آن هم مسایل امنیتی به وجود آمده و آلوده شدن این ریپازیتوری است . همچنین توصیه کرده است تا به جای jcenter از maven central استفاده شود.* همچنین نکته ی مهم دیگر این است که برای افزایش سرعت گریدل تا جای ممکن تعداد مخازن خود را اندک نگه دارید.* و به عنوان نکته ی مهم آخر ، قاعده ی گفته شده برای مخازن پلاگین های گریدل نیز صادق است . تقریبا تمام پلاگین هایی که در اندروید استفاده می شود در دو مخزن Google و Maven Central موجود است اما اگر به هر دلیل به مخزن شخص ثالث gradlePortalPlugin احتیاج داشتید آن را در آخر بلاک ریپازیتوری و پایین تر از دو مخزن دیگر تعریف کنید .9) Use static dependency versionsاز راه های دیگر افزایش سرعت گریدل حذف داینامیک ورژن ها ( " 2.+ " ) است . داینامیک ورژن ها باعث می شوند تا گریدل دایما با مخازن در ارتباط باشد تا بتواند جدید ترین ورژن ها را پیدا کند.گریدل به طور پیشفرض یکبار در هر 24 ساعت با مخازن ارتباط برقرار می کند تا از اخرین تغییرات مطلع شود.10) JVM parallel garbage collectorیکی از مواردی که می تواند به سرعت اجرای اپ و گریدل شما کمک کند استفاده از parallel GC است .هر بار که زباله جمع کن فعال می شود می تواند هزینه ی قابل توجهی مانند بلاک شدن لحظه ای Main Thread را به پروژه شما اعمل کند .به منظور کاهش مدت زمان اجرای GC می توانید کد زیر را به گریدل اضافه کنید : org.gradle.jvmargs=-XX:+UseParallelGCتا این جای کار با مهم ترین روش های بهینه سازی گریدل آشنا شدیم ، البته بحث به این جا ختم نمی شود اگر مایلید تا با موارد بیش تری از جمله بهینه سازی های زمان اجرا در تست ها اشنا بشید می توانید این لینک را بررسی کنید. با مهم ترین موارد تئوری گریدل آشنا شدیم ، اکنون می خواهیم تا دست به کد شویم و برای گریدل تعدادی تسک بنویسیم .در نوشتن تسک ها از زبان کاتلین استفاده می کنیم زیرا سینتکس groovy برای عمده ی ما نا آشناست و همچنین سرعت اجرای تسک های نوشته شده به زبان کاتلین و جاوا بیش تره .برای اینکه بتونیم از سینتکس کاتلین در گریدل استفاده کنیم نیاز داریم که بیاد گریدل هامون رو به فرم kts بنویسیم اگر تا حالا تجربه ی kts کردن گریدلتون رو نداشتید این لینک رو دنبال کنید (همچنین با این کار می توانید امکانات جالبی را به گریدل خودتان اضافه کنید) برای این قسمت لازمه تا به طور کامل با مفاهیم فاز های گریدل آشنا باشید . تسک نویسی (Authoring Tasks)ابتدا با یک تسک ساده شروع میکنیم . محلی که در آن تسک ها تعریف می شوند همان فایل build.gradle.kts است .task('printHello1') { println('hello world1') }برای تعریف این تسک از ("name")task استفاده کرده ایم سپس در بلاک آن تابع println از کاتلین را فراخوانی کرده ایم . برای اجرای این تسک می توانید با دوبار کلیک روی کلید CTRL در ide های شرکت intelij و نوشتن دستور زیر ، تسک را اجرا کنید gradle printHello1 //windows ./gradle printHello1 //linuxدر دستور بالا printHello همان نامی است که برای این تسک تعریف کرده ایم . در واقع نام هر تسک شناسه ای برای اجرای آن تسک است .خروجی این تسک به این صورت در کنسول نمایش داده می شود :برای تعریف تسک ها در گریدل دو روش وجود دارد :1) تعریف تسک به صورت ("name")task که در بالا مثالی از آن دیدیم 2) تعریف تسک به صورت ("name")tsaks.register که پایین تر مثال هایی از آن می بینیم اما تفاوت این روش ها در چیست : روش اول هنگامی استفاده می شود که تسک ما یک تسک کوچک است همانند همین تسک hello worldاما روش دوم در تسک های پیچیده و بزرگ استفاده می شود .زیرا روش دوم از مکانیسم lazy evaluation استفاده می کند به این معنا که تسک فقط زمانی ساخته می شود که به آن نیاز باشد اما در روش اول به محض خوانده شدن build.gradle تسک نیز ساخته می شود .بنابراین به طور کلی ما از روش دوم برای تعریف تسک ها استفاده می کنیم .مثال hello world با روش رجیستر : tasks.register('printHello2') { println('hello world2') }تا این جا با نحوه ی تعریف تسک ها آشنا شدیم حالا میریم سراغ مورد بعدی :doFirst{} and doLast{} methods : فرض کنید دو تسک زیر را در گریدل تعریف کرده ایم :tasks.register('printHello1') { println('hello world1') } tasks.register('printHello2') { println('hello world2') }حالا تسک printHello2 را اجرا می کنیم ، انتظار داریم تا عبارت "hello world2" چاپ شود !عبارت چاپ می شود اما یک مشکلی به وجود می آید عبارت "hello world1" هم چاپ می شود اگر تسک 1 را هم اجرا کنیم هلو ورد 2 نیز چاپ میشود .ما فقط یک تسک را اجرا کرده ایم اما چرا تسک دیگر نیز ران می شود ؟اصولا نوشتن تسک ها به فرم بالا یک اشتباه هست ما باید دستور println را که اکشن تسک هایمان است را در بلاک های doFirst یا doLast بنویسیم مانند کد زیر:tasks.register('printHello1') { doFirst{ println('hello world1') } } tasks.register('printHello2') { doFirst{ println('hello world2') } }اما چرا ؟هنگامی که تسک ها را بدون بلاک های doFirst یا doLast بنویسیم اجرای تسک ها در فاز Configuration اتفاق می افتد (یعنی به محض جنریت شدن گراف ، تسک ران می شود .)همانطور که مطلع هستید هر تسکی که در گریدل اجرا می کنید به ازای آن تسک یکبار دیگر تمام build script ها خوانده می شود (فاز Initialization ) و مجددا گراف تسک ها از ابتدا ایجاد می شود (فاز Configuration) تنها فاز Execution است که به ازای هر تسک به صورت اختصاصی ران می شود .بنابراین هنگامی که دستور println در فاز کانفیگ اجرا می شود به ازای تمام تسک ها ران می شود(حتی اگر تسک دیگری ران شده باشد).بنابراین هر گاه تسکی می نویسیم اکشن آن را داخل بلاک های doFirst و doLast می نویسیم تا لایف سایکل اکشن تسک (به طور مثال println) به فاز Execution منتقل شود (این باعث می شود تا اکشن تسک فقط زمانی اجرا شود که تسک آن ران شده باشد. )تفاوت doFirst با doLast چیست ؟ بلاک doFirst اولین کار هایی که در حین اجرای یک تسک باید انجام شود را مشخص می کند .بلاک doLast اخرین کار هایی که در حین اجرای یک تسک باید انجام شود را مشخص می کند .در واقع این دو متد برای مشخص کردن اولویت اجرا وظایف در یک تسک به کار می روند ، به طور مثال در یک تسک می خواهیم کار A حتما قبل از B انجام شود پس به این صورت کد آن را می نویسیم :task.register('name'){ doFirst{ A } doLast{ B } }اما در مثال println که بالاتر آوردیم تفاوتی در استفاده ی doFirst یا doLast مطرح نیست از هر کدام که مایل باشیم می توانیم استفاده کنیم .تاکنون موفق شدیم یک تسک ساده بنویسیم و با مفاهیم doFirst , doLast آشنا بشیم ، اکنون برای جمع بندی این قسمت یک تسک پیچیده تر برای گریدل می نویسیم:کار کردن با فایل ها در گریدل یکی از تسک های بسیار مرسوم است ، اکنون میخواهیم یک تسک بنویسیم تا در روت پروژه یک دایرکتوری به نام generate ایجاد کند (اگر وجود نداشت) و در این دایرکتوری یک فایل با نام log.txt ایجاد کند سپس داخل فایل txt عبارت version = 1.0.0 را بنویسد . کد این تسک به صورت زیر است :task('createLogFile') { doLast { val targetDir = layout.projectDirectory.dir('/generate').toString() 1️⃣ File(targetDir).mkdirs() 2️⃣ File(targetDir, 'log.txt').writeText('version = 1.0.0') 3️⃣ } }1️⃣ : در قسمت اول می خواهیم تا محل قرار گیری پروژه را پیدا کنیم (این محل در سیستم های مختلف متفاوت است) به کمک layout.projectDirectory محل قرار گیری پروژه را پیدا می کنیم .خروجی این دستور در سیستم من به این صورت است : C:\Users\mohsen\IdeaProjects\GradleCustomTask به طور کلی هر گاه به دنبال آدرس پروژه ی خودمان بودیم از ابزار layout استفاده می کنیم .اکنون به کمک متد dir دایرکتوری generate را به مسیر اضافه کرده ایم (به علامت / دقت کنید)و در نهایت کل این مسیر را به یک رشته تبدیل کرده ایم.2️⃣ : در این قسمت File را از پکیج java فراخوانی کرده ایم و مسیری را که در 1️⃣ ایجاد کرده بودیم را به عنوان آرگومان به آن داده ایم . سپس با کال کردن متد mkdirs دایرکتوری generate را ایجاد کرده ایم.3️⃣ : حالا در قسمت اخر مجددا File را فراخوانی کرده و محل قرار گیری فایل و اسم و فرمت آن را به عنوان آرگومان داده ایم این عبارت فایل log.txt را در مسیر دایرکتوری generate ایجاد می کند.در انتهای کار هم با متد writeText عبارتی را که میخواستیم در فایل Log نوشته ایم.تسک های پیشفرض (built In Tasks)تیم گریدل تسک های پر تکرار و کاربردی را به صورت پیشفرض در گریدل ایجاد کرده است که برای کار های مختلف می توانیم آن ها را بسط داده و از آن های استفاده کنیم . به عناون مثال می توان به تسک های Delete , Copy , Jar , Compress و ... اشاره کرد .در این مقاله با برخی از این تسک ها کار خواهیم کرد .تسک (Copy)فرض کنید که فایل log.txt که در تسک قبل ایجاد کردیم را میخواهیم در یک لوکیشن دیگر کپی کنیم و یا حتی بعد از کپی نام آن را عوض کنیم ، می توانیم با استفاده از File مجددا این کار را انجام دهیم ، اما به منظور راحتی کار از تسک Copy استفاده می کنیم . ساختار این تسک به شکل زیر است :task('copyLogFile', Copy::class) { 0️⃣ val sourceDir = layout.projectDirectory.dir('/generate/log.txt').toString() 1️⃣ val targetDir = layout.projectDirectory.dir('/src/main/resources').toString() 2️⃣ from(sourceDir) 3️⃣ into(targetDir) 4️⃣ rename('log.txt', 'ConfLog.txt') 5️⃣ }0️⃣ : به نحوه ساخت این تسک توجه کنید برخلاف تسک های قبلی یک ارگومان جدید به نام Copy::class پاس داده شده است (این کلاس از پکیج gradle ایمپورت شده است). به این ترتیب در این تسک ما می توانیم از Api های تسک Copy نیز استفاده کنیم .1️⃣ : در قسمت یک محل قرار گیری فایل log.txt مشخص شده است . 2️⃣ : در قسمت دوم جایی که می خواهیم فایل در آن کپی شود را مشخص کرده ایم .3️⃣ : قسمت 3 به دلیل وجود تسک Copy در دسترس است . متد from محل قرار گیری فایل را دریافت می کند.4️⃣ : قسمت 4 به دلیل وجود تسک Copy در دسترس است . متد into محلی که میخواهیم فایل در آن کپی شود را دریافت می کند .تا این جای کار اگر تسک را اجرا کنیم فایل log.txt کپی می شود اما :5️⃣ : در قسمت 5 که به دلیل وجود تسک Copy در دسترس است نام فایلی که میخواهد کپی شود را از log.txt به ConfLog.txt تغییر دادیم .اکنون بعد از اجرا فایل با نام جدی کپی میشود. تسک (Delete)آیا تمایل دارید تا فایل log.txt را که از آن یک کپی گرفتیم را با یک تسک دیگر پاک کنیم . تسک زیر این کار را انجام می دهد :task('deleteLogFile', Delete::class) { val targetDir = layout.projectDirectory.dir('/generate/log.txt').toString() delete(targetDir) } اکنون باید بدانید که این تسک چگونه کار می کند.تعریف وابستگی در تسک ها تسک های createLogFile و copyLogFile و deleteLogFile را که در قسمت قبل بررسی کردیم را به خاطر بیاورید . ما این تسک ها را استپ به استپ به صورت دستی اجرا می کردیم تا نتایج آن را مشاهده کنیم .اکنون می خواهیم کاری کنم تا وقتی تسک createLogFile انجام شد و فایل txt جنریت شد تسک copyLogFile به طور خودکار اجرا شود و از فایل کپی بگیرد ، سپس بعد از کپی شدن فایل، تسک deleteLogFile به طور خودکار اجرا شود و txt اصلی را پاک کند .قبلا در رابطه با گراف تسک ها صحبت کرده ایم برای انجام این کار باید در فاز کانفیگ در تسک ها وابستگی ایجاد کنیم تا در گراف ، تسک ها به صورت وابسته تعریف شوند .قبل از اینکه تسک های فایلمون رو وابسته کنیم انواع روش های تعریف وابستگی را توضیح می دهیم .وابستگی به کمک (depend on) مثال زیر را در نظر بگیرید :tasks.register('printHello1') { doFirst{ println('hello world1') } } tasks.register('printHello2') { doFirst{ println('hello world2') } dependsOn('printHello1') }در این مثال ما اجرای تسک printHello2 را وابسته به اجرای printHello1 کرده ایم ، یعنی اگر تسک printHello2 را اجرا کنیم قبل از اجرای این تسک ، تسک printHello1 اجرا می شود . ما این کار را به کمک متد dependsOn انجام دادیم ، ورودی این متد نام یک تسک دیگر است.اکنون یک سوال کنکوری **** :چرا dependOn را خارج از بلاک doFirst تعریف کرده ایم ؟قبلا گفتیم که بلاک doFirst لایف سایکل را به فاز Execution منقل می کند . فاز Execution بعد از فاز کانفیگ انجام می شود یعنی جایی که گراف تسک ها دیگر ساخته شده است .ما دیپندسی را فقط در فاز کانفیگ می توانیم ایجاد کنیم یعنی جایی که گراف قرار است ساخته شود.اگر در حین اجرای یک تسک بخواهیم تغییری در گراف ایجاد کنیم به آن معناست که گراف invalid شده است *بنابراین حتی اجرای تسک جاری نیز زیر سوال می رود بنابراین گریدل ارور زیر را برای شما ایجاد می کند.وابستگی به کمک (finalizedBy) قبل از اینکه به این قسمت برسیم اجازه بدید تا همین جا یک نکته را توضیح بدهم:همونطور که قبلا مطرح کردیم گریدل شامل فاز های متفاوتی است این به آن معناست که گریدل شامل یک لایف سایکل مشخص است و مثل هر سیستم دیگری که لایف سایکل دارد می تواند callback های مختلفی داشته باشد .به طور مثال لیستنر های زیر را در نظر بگیرید که همگی در فایل build.gradle تعریف شده اند و متناسب با کار خود می توانیم از آن ها استفاده کنیم :gradle.taskGraph.whenReady { } gradle.taskGraph.afterTask{ } gradle.taskGraph.addTaskExecutionGraphListener { } gradle.buildFinished { } gradle.projectsLoaded { }اکنون کد زیر را برای تعریف دیپندنسی در نظر بگیرید : tasks.whenTaskAdded { 1️⃣ if (this.name == 'printHello1') { 2️⃣ this.finalizedBy('printHello2') 3️⃣ } } tasks.register('printHello1') { doFirst{ println('hello world1') } } tasks.register('printHello2') { doFirst{ println('hello world2') } }1️⃣ : در قسمت یک ما روی تسک ها لیستنر whenTaskAdded را ست کرده ایم ، این لیستنر هنگامی که هر تسک ساخته و add شد در فاز کانفیگ کال می شود .(توجه کنید که در فاز اجرا تعریف وابستگی غیر ممکن است.)2️⃣ : در قسمت دوم بررسی کرده ایم که آیا تسک ساخته شده همان تسک printHello1 هست یا خیر.3️⃣ : در قسمت سوم بیان کرده ایم که اگر تسک ما printHello1 بود انجام آن تسک را با اجرای تسک printHello2 به پایان برساند (درواقع بعد از اجرای موفقیت آمیز printHello1 ، تسک printHello2 اجرا می شود )این وابستگی به کمک یک finalizer اتفاق می افتد .کد بالا به منظور آشنایی شما با لیستنر ها مطرح شد اما آیا نمیشد به همان روشی که از depend استفاده کردیم از finalizer هم استفاده کنیم ؟ پاسخ بله است ?، به کد زیر دقت کنید :tasks.register('printHello1') { doFirst{ println('hello world1') } finalizedBy('printHello2') } tasks.register('printHello2') { doFirst{ println('hello world2') } }تفاوت finalizedBy با depend on چیست ؟به عقب برگردید و کد ها را دوباره بررسی کنید حدس میزنید که تفاوت در چیست ؟فرض کنید که گریدل ما شامل تسک های A , B است :میخواهیم بگوییم که اگر تسک A انجام شد بعد از آن تسک B را هم انجام بده -> این همان کاربرد finalizeBy است .میخواهیم بگوییم که اگر تسک B اجرا شد قبل از اجرای آن تسک A را ابتدا انجام بده -> این همان کاربرد dependOn است .حالا مثال تسک های کار با فایل را که کد آن را در بالا آوردیم به خاطر بیاوردید برای اینکه وقتی تسک createLogFile انجام شد و فایل txt جنریت شد تسک copyLogFile به طور خودکار اجرا شود و از فایل کپی بگیرد ، سپس بعد از کپی شدن فایل، تسک deleteLogFile به طور خودکار اجرا شود و txt اصلی را پاک کند باید از کدام روش استفاده کنیم ؟کد زیر را بررسی کنید :task('createLogFile') { doLast { val targetDir = layout.projectDirectory.dir('/generate').toString() File(targetDir).mkdirs() File(targetDir, 'log.txt').writeText('version = 1.0.0') } finalizedBy('copyLogFile') } task('copyLogFile', Copy::class) { val sourceDir = layout.projectDirectory.dir('/generate/log.txt').toString() val targetDir = layout.projectDirectory.dir('/src/main/resources').toString() from(sourceDir) into(targetDir) rename('log.txt' , 'ConfLog.txt') finalizedBy('deleteLogFile') } task('deleteLogFile', Delete::class) { val targetDir = layout.projectDirectory.dir('/generate/log.txt').toString() delete(targetDir) }اکنون با اجرای تسک createLogFile تمام تسک ها به صورت خودکار و زنجیر وار اجرا می شوند .اگر متوجه نشدید که چرا باید از روش finalize استفاده کنیم برگردید و دوباره کد هارا بررسی کنید و در نظر داشته باشید که اگر میخواستیم از dependOn استفاده کنیم باید تسک آخر (deleteLogFile) را اجرا می کردیم تا بتوانیم همین زنجیره را ایجاد کنیم .نکته ی کنکوری ****:در تصویر بالا مقابل تسک copyLogFile تگ UP_TO_DATE خورده است این به چه معناست ؟یکبار دیگر مکانیسم های کش گریدل را به خاطر بیاورید ?دلیل این اتفاق این است که من یکبار دیگر این تسک را اجرا کرده بودم و فایل ConfLog.txt در پوشه ی ریسورس ها موجود بود ، بنابراین گریدل مجددا آن را اجرا نکرد و صرفا از خروجی آن برای تسک بعدی استفاده کرد.تا این جای کار دانش خوبی در زمینه ی تسک های گریدل به دست آوردیم ، یکبار دیگه مباحث رو تو ذهنتون مرور کنید که میخواهیم عمق کار رو بیش تر کنیم تسک های پیشرفته (enhanced tasks)تا این جای کار تسک هامون رو مستقیم در فایل build.gradle تعریف میکردیم ، اما این تنها راه تعریف تسک ها نیست . اکنون با یک روش جدید آشنا شوید : open class PrintTask : DefaultTask() { @TaskAction fun sayHello() { println('hello from PrintTask') } } متوجه شدید که میخواهیم چه کار کنیم ؟ قصد داریم تا تسک ها رو به صورت کد نویسی معمول انجام بدیم پس گام به گام نوشتن این نوع از تسک ها رو بررسی می کنیم :گام اول : ابتدا buildSrc را ایجاد می کنیم . اما buildSrc چیست ؟ اگر بیلد گریدل خودتون رو به فرم kotlin dsl (kts) در آورده باشید احتمالا با این پوشه آشنا هستید .پوشه ی buildSrc یک entry point برای گریدل است . (در واقع گریدل در هر بیلد محتوای این پوشه را می خواند و کد های آن را تفسیر می کند)ما به این پوشه احتیاج داریم تا کد های تسک مون رو داخلش قرار بدیم ،نحوه ی ایجاد buildSrc : در داخل root پروژه ی خودتون یک دایرکتوری با نام buildSrc ایجاد کنیدسپس یکی از مسیر های زیر را در آن ایجاد کنید (وابسته به زبانی که در آن کد می زنید):rootProjectDir/buildSrc/src/main/groovyیاrootProjectDir/buildSrc/src/main/javaیا rootProjectDir/buildSrc/src/main/kotlinحالا یکبار گریدل رو سینک کنید اگر همه چیز درست باشد پوشه های .gradle و build برای شما جنریت می شود.گام دوم : در مسیری که در buildSrc ایجاد کردید کلاس خود را بنویسید (مانند PrintTask ) سپس آن را از ()DefaultTask اکستند کنید . **این کلاس حتما باید open باشد .**گام سوم : متدی را که در این کلاس اکشن شما را انجام می دهد بنویسید و با انوتیشن @TaskAction علامت گذاری کنید .گام چهارم : اکنون برای کانفیگ و اجرای این تسک ، باید آن را در build.gradle رجیستر کنیم:task('sayHello' , PrintTask ::class) {} یا tasks.register( 'sayHello' , PrintTask ::class)به عنوان ارگومان اول یک اسم برای تسک انتخاب کرده که برای اجرای تسک از همین اسم استفاده می شود .(این اسم ترجیحا بهتر است تا با نام فانکشن TaskAction یکسان باشد )و به عنوان آرگومان دوم کلاسی را که ایجاد کردیم معرفی می کنیم .حالا با اجرای این تسک عبارت برای ما پرینت می شود .اما یک سوال : شاید بپرسید که چه لزومی داره تا به این روش تسک هارو بنویسیم ?همونطور که متوجه شدید مهم ترین مزیت این روش این بود که ما اکشن تسک را به جای اینکه در فایل build.gradle بنویسیم در یک متد جداگانه نوشتیم بنابراین از شلوغ شدن build.gradle جلوگیری کردیم و در آینده با پلاگین نوشتن بر روی این تسک می توانیم آن را با دیگران به اشتراک بگذاریم (تقریبا تمام تسک هایی که لایبرری های شخص ثالث به گریدل شما اضافه میکنند به این روش نوشته شدن)ارسال آرگومان به enhanced tasksبرای اینکه از طریق build.gradle مقادیری را برای تسک ارسال کنیم می توانیم از روش زیر استفاده کنیم :open class PrintTask : DefaultTask() { @Input var myName: String = ' ' @TaskAction fun sayHello() { println('hello $myName') } }همچنین در build.gradle:task('sayHello' , PrintTask ::class) { myName = 'Mohsen' }به کمک انوتیشن @Input می توانیم هر مقداری را از build.gradle ارسال کنیم .حالا برای اینکه شما هم خسته نشید و مطلب رو کم کم جمع بندی کنیم ، با هم یک تسک مینویسیم و اون رو بررسی میکنیم :فرض کنید ما میخواهیم یک قرارداد در شرکتمون بنویسیم که طبق این قرارداد هیچ کس حق نداره از عکس ها با فرمت JPG در پروژه های اندرویدی مون استفاده کنه و اگر کسی این شرط رو نقض کرد گریدل ارور بده (یا یک ایمیل رو به لید پروژه ارسال کنه ) . کد این تسک به صورت زیر است :open class ExtensionFilterTask : DefaultTask() { @Input var resDir: String = ' ' @Input var taskEnabled: Boolean= true init { enabled = taskEnabled } @TaskAction fun findJPGExtension() { val file = File(resDir) file.list()?.forEach { fileName -> if (fileName.contains('.JPG')) { throw GradleException('Invalid Extension') } } } }build.gradle :tasks.register( 'findJPGExtension', ExtensionFilterTask::class ){ resDir = layout.projectDirectory.dir('/src/main/resources/drawable').toString() taskEnabled = true } ابتدا کلاس ExtensionFilterTask را ایجاد کرده و ورودی های resDir که همان آدرس دایرکتوری resource/drawable ها است و taskEnabled را که مشخص می کند که تسک اجرا شود یا خیر را برای آن تعریف می کنیم .سپس در بلاک init مقدار enabled را ست می کنیم (مشخص می کند که تسک اجرا شود یا خیر)سپس متد اکشن را ایجاد کرده و داخل آن یک ابجکت از File با مسیر resDir مشخص می کنیم متد list از ابجکت File اسم تمام فایل های موجود در دایرکتوری resDir را بر می گرداند سپس با ایجاد یک حلقه ی for روی این لیست بررسی میکنیم که آیا فایلی با پسوند JPG وجود دارد یا خیر در صورت وجود چنین فایلی گریدل یک اکسپشن با مسیج Invalid Extension پرتاب می کند .ما تا این جا نکات مهم و کلیدی در رابطه با گریدل مطرح کردیم و شما بعد از خوندن این مقاله دانش خوبی در این رابطه پیدا می کنید ، اما این تازه شروع راهه ! گریدل یک ابزار قدرتمنده شما می تونید تسک های مختلفی رو برای گریدل ایجاد کنید مثلا تسکی بنویسید که ایمیل ارسال کنه ، کامند ران کنه ، با نتورک ارتباط برقرار کنه ، برای شما فایل ارسال کنه ، کد های شما روبخونه و کد های دیپریکیت شده رو لیست کنه و گزارش بده ، تست های خاصی رو اجرا کنه و ...اگر مشتاق بودید تا در این رابطه بیش تر بدونید و پیش برید داکیومنت اصلی گریدل رو از دست ندید .ممنون که تا انتهای این مقاله طولانی همراه من بودید .??